Skip to content

Commit

Permalink
Add support for metric endpoints containing hostnames and ports [#167…
Browse files Browse the repository at this point in the history
…761792]

Signed-off-by: Jake Schuenke <[email protected]>
  • Loading branch information
thepeterstone authored and Jake Schuenke committed Aug 13, 2019
1 parent 6654b32 commit fd4c5cf
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 41 deletions.
8 changes: 8 additions & 0 deletions command/command_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ func newMockCliConnection() *mockCliConnection {
cliCommandsCalled: make(chan []string, 10),
getAppResult: plugin_models.GetAppModel{
Guid: "app-guid",
Name: "app-name",
Routes: []plugin_models.GetApp_RouteSummary{{
Host: "app-host",
Domain: plugin_models.GetApp_DomainFields{
Name: "app-domain",
},
Path: "app-path",
}},
},
}
}
Expand Down
79 changes: 72 additions & 7 deletions command/register.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,80 @@
package command

import (
"crypto/sha1"
"encoding/base64"
"fmt"
"net/url"
"strings"

pluginmodels "code.cloudfoundry.org/cli/plugin/models"
)

func RegisterLogFormat(cliConn cliCommandRunner, appName, logFormat string) error {
func RegisterLogFormat(cliConn cliCommandRunner, appName, logFormat string) error {
return ensureServiceAndBind(cliConn, appName, structuredFormat, logFormat)
}

func RegisterMetricsEndpoint(cliConn cliCommandRunner, appName, path string) error {
return ensureServiceAndBind(cliConn, appName, metricsEndpoint, path)
func RegisterMetricsEndpoint(cliConn cliCommandRunner, appName, route string) error {
if route[0] != '/' {
app, err := cliConn.GetApp(appName)
if err != nil {
return err
}

err = validateRouteForApp(route, app)
if err != nil {
return err
}

}
return ensureServiceAndBind(cliConn, appName, metricsEndpoint, route)
}

func validateRouteForApp(requestedRoute string, app pluginmodels.GetAppModel) error {
requested, err := url.Parse(ensureHttpsPrefix(requestedRoute))
if err != nil {
return fmt.Errorf("unable to parse requested route: %s", err)
}
for _, r := range app.Routes {
var host string
host = formatHost(r)
route := &url.URL{
Host: host,
Path: "/" + r.Path,
}

if requested.Host == route.Host && strings.HasPrefix(requested.Path, route.Path) {
return nil
}
}
return fmt.Errorf("route '%s' is not bound to app '%s'", requestedRoute, app.Name)
}

func formatHost(r pluginmodels.GetApp_RouteSummary) string {
host := r.Domain.Name
if r.Host != "" {
host = fmt.Sprintf("%s.%s", r.Host, host)
}
if r.Port != 0 {
host = fmt.Sprintf("%s:%d", host, r.Port)
}
return host
}

func ensureHttpsPrefix(requestedRoute string) string {
return "https://" + strings.Replace(requestedRoute, "https://", "", 1)
}

func ensureServiceAndBind(cliConn cliCommandRunner, appName, serviceProtocol, config string) error {
cleanedConfig := strings.Trim(strings.Replace(config, "/", "-", -1), "-")
serviceName := serviceProtocol + "-" + cleanedConfig
serviceName := generateServiceName(serviceProtocol, config)
exists, err := findExistingService(cliConn, serviceName)
if err != nil {
return err
}

if !exists {
binding := serviceProtocol + "://" + config
_, err := cliConn.CliCommandWithoutTerminalOutput("create-user-provided-service", serviceName, "-l", binding)
_, err = cliConn.CliCommandWithoutTerminalOutput("create-user-provided-service", serviceName, "-l", binding)
if err != nil {
return err
}
Expand All @@ -33,6 +85,19 @@ func ensureServiceAndBind(cliConn cliCommandRunner, appName, serviceProtocol, co
return err
}

func generateServiceName(serviceProtocol string, config string) string {
cleanedConfig := strings.Trim(strings.Replace(config, "/", "-", -1), "-")
serviceName := serviceProtocol + "-" + cleanedConfig
// Cloud Controller limits service name lengths:
// see https://github.com/cloudfoundry/cloud_controller_ng/blob/master/vendor/errors/v2.yml#L231
if len(serviceName) > 50 {
hasher := sha1.New()
hasher.Write([]byte(cleanedConfig))
serviceName = serviceProtocol + "-" + strings.Trim(base64.URLEncoding.EncodeToString(hasher.Sum(nil)), "=")
}
return serviceName
}

func findExistingService(cliConn cliCommandRunner, serviceName string) (bool, error) {
existingServices, err := cliConn.GetServices()
if err != nil {
Expand All @@ -45,4 +110,4 @@ func findExistingService(cliConn cliCommandRunner, serviceName string) (bool, er
}
}
return false, nil
}
}
153 changes: 119 additions & 34 deletions command/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package command_test
import (
"errors"

"code.cloudfoundry.org/cli/plugin/models"
"github.com/pivotal-cf/metric-registrar-cli/command"

"code.cloudfoundry.org/cli/plugin/models"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"
)

var _ = Describe("Register", func() {
Expand All @@ -17,18 +18,16 @@ var _ = Describe("Register", func() {

err := command.RegisterLogFormat(cliConnection, "app-name", "format-name")
Expect(err).ToNot(HaveOccurred())
Expect(cliConnection.cliCommandsCalled).To(Receive(ConsistOf(
"create-user-provided-service",
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService(
"structured-format-format-name",
"-l",
"structured-format://format-name",
)))
))

Expect(cliConnection.cliCommandsCalled).To(Receive(ConsistOf(
"bind-service",
Expect(cliConnection.cliCommandsCalled).To(receiveBindService(
"app-name",
"structured-format-format-name",
)))
))
})

It("doesn't create a service if service already present", func() {
Expand All @@ -39,7 +38,7 @@ var _ = Describe("Register", func() {

err := command.RegisterLogFormat(cliConnection, "app-name", "config")
Expect(err).ToNot(HaveOccurred())
Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("bind-service")))
Expect(cliConnection.cliCommandsCalled).To(receiveBindService())
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

Expand All @@ -57,7 +56,7 @@ var _ = Describe("Register", func() {

Expect(command.RegisterLogFormat(cliConnection, "app-name", "config")).ToNot(Succeed())

Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("create-user-provided-service")))
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService())
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

Expand All @@ -67,40 +66,68 @@ var _ = Describe("Register", func() {

Expect(command.RegisterLogFormat(cliConnection, "app-name", "config")).ToNot(Succeed())

Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("create-user-provided-service")))
Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("bind-service")))
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService())
Expect(cliConnection.cliCommandsCalled).To(receiveBindService())
})
})

Context("RegisterMetricsEndpoint", func() {
It("creates a service", func() {
It("creates a service given a path", func() {
cliConnection := newMockCliConnection()

err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "endpoint")
err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "/metrics")
Expect(err).ToNot(HaveOccurred())
Expect(cliConnection.cliCommandsCalled).To(Receive(ConsistOf(
"create-user-provided-service",
"metrics-endpoint-endpoint",
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService(
"metrics-endpoint-metrics",
"-l",
"metrics-endpoint://endpoint",
)))
"metrics-endpoint:///metrics",
))

Expect(cliConnection.cliCommandsCalled).To(Receive(ConsistOf(
"bind-service",
Expect(cliConnection.cliCommandsCalled).To(receiveBindService(
"app-name",
"metrics-endpoint-endpoint",
)))
"metrics-endpoint-metrics",
))
})

It("creates a service given a route", func() {
cliConnection := newMockCliConnection()

err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "app-host.app-domain/app-path/metrics")
Expect(err).ToNot(HaveOccurred())
serviceName, endpoint := expectToReceiveCupsArgs(cliConnection.cliCommandsCalled)
Expect(serviceName).To(HavePrefix("metrics-endpoint-"))
Expect(endpoint).To(Equal("metrics-endpoint://app-host.app-domain/app-path/metrics"))

Expect(cliConnection.cliCommandsCalled).To(receiveBindService())
})

It("checks the route", func() {
cliConnection := newMockCliConnection()
err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "not-app-host.app-domain/app-path/metrics")

Expect(err).To(MatchError("route 'not-app-host.app-domain/app-path/metrics' is not bound to app 'app-name'"))
})

It("does not use service names longer than 50 characters", func() {
cliConnection := newMockCliConnection()

err := command.RegisterMetricsEndpoint(cliConnection, "very-long-app-name-with-many-characters", "/metrics")
Expect(err).ToNot(HaveOccurred())
serviceName, _ := expectToReceiveCupsArgs(cliConnection.cliCommandsCalled)
Expect(len(serviceName)).To(BeNumerically("<=", 50))

Expect(cliConnection.cliCommandsCalled).To(receiveBindService())
})

It("doesn't create a service if service already present", func() {
cliConnection := newMockCliConnection()
cliConnection.getServicesResult = []plugin_models.GetServices_Model{
{Name: "metrics-endpoint-config"},
{Name: "metrics-endpoint-metrics"},
}

err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "config")
err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "/metrics")
Expect(err).ToNot(HaveOccurred())
Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("bind-service")))
Expect(cliConnection.cliCommandsCalled).To(receiveBindService())
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

Expand All @@ -109,40 +136,98 @@ var _ = Describe("Register", func() {

err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "/v2/path/")
Expect(err).ToNot(HaveOccurred())
Expect(cliConnection.cliCommandsCalled).To(Receive(ConsistOf(
"create-user-provided-service",
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService(
"metrics-endpoint-v2-path",
"-l",
"metrics-endpoint:///v2/path/",
)))
))
})

It("parses routes without hosts correctly", func() {
cliConnection := newMockCliConnection()

cliConnection.getAppResult.Routes = []plugin_models.GetApp_RouteSummary{{
Host: "",
Domain: plugin_models.GetApp_DomainFields{
Name: "tcp.app-domain",
},
}}

Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "tcp.app-domain/v2/path/")).To(Succeed())
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService(
"metrics-endpoint-tcp.app-domain-v2-path",
"-l",
"metrics-endpoint://tcp.app-domain/v2/path/",
))
})

It("returns error if getting the app fails", func() {
cliConnection := newMockCliConnection()
cliConnection.getAppError = errors.New("error")

Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "app-host.app-domain/app-path/metrics")).ToNot(Succeed())
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

It("returns an error if parsing the route fails", func() {
cliConnection := newMockCliConnection()

err := command.RegisterMetricsEndpoint(cliConnection, "app-name", "#$%#$%#")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(HavePrefix("unable to parse requested route:"))
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

It("returns error if getting the service fails", func() {
cliConnection := newMockCliConnection()
cliConnection.getServicesError = errors.New("error")

Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "config")).ToNot(Succeed())
Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "/metrics")).ToNot(Succeed())
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

It("returns error if creating the service fails", func() {
cliConnection := newMockCliConnection()
cliConnection.cliErrorCommand = "create-user-provided-service"

Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "config")).ToNot(Succeed())
Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "/metrics")).ToNot(Succeed())

Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("create-user-provided-service")))
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService())
Expect(cliConnection.cliCommandsCalled).ToNot(Receive())
})

It("returns error if binding fails", func() {
cliConnection := newMockCliConnection()
cliConnection.cliErrorCommand = "bind-service"

Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "config")).ToNot(Succeed())
Expect(command.RegisterMetricsEndpoint(cliConnection, "app-name", "/metrics")).ToNot(Succeed())

Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("create-user-provided-service")))
Expect(cliConnection.cliCommandsCalled).To(Receive(ContainElement("bind-service")))
Expect(cliConnection.cliCommandsCalled).To(receiveCreateUserProvidedService())
Expect(cliConnection.cliCommandsCalled).To(receiveBindService())
})
})
})

func expectToReceiveCupsArgs(called chan []string) (string, string) {
var args []string
Expect(called).To(Receive(&args))
Expect(args).To(HaveLen(4))
Expect(args[0]).To(Equal("create-user-provided-service"))
Expect(args[2]).To(Equal("-l"))
return args[1], args[3]
}

func receiveCreateUserProvidedService(args ...string) types.GomegaMatcher {
if len(args) == 0 {
return Receive(ContainElement("create-user-provided-service"))
}

return Receive(Equal(append([]string{"create-user-provided-service"}, args...)))
}

func receiveBindService(args ...string) types.GomegaMatcher {
if len(args) == 0 {
return Receive(ContainElement("bind-service"))
}
return Receive(Equal(append([]string{"bind-service"}, args...)))
}

0 comments on commit fd4c5cf

Please sign in to comment.