Skip to content

Commit

Permalink
viz: add viz profile command (linkerd#5621)
Browse files Browse the repository at this point in the history
## What this changes

This adds a `viz profile` command that outputs a service profile based off tap
data. It is identical—but fixes—the current `profile --tap` command.

Additionally, it removes the `--tap` flag from the `profile` command since this
depends on the Viz extension being installed in order to tap a service.

## Why

The `profile --tap` command is currently broken since it depends on the Viz
extension being installed, but the `profile` command is part of the core
install.

Closes linkerd#5613

Unblocks linkerd#5545

Signed-off-by: Kevin Leimkuhler <kevin@kleimkuhler.com>
Signed-off-by: Jijeesh <jijeesh.ka@gmail.com>
  • Loading branch information
kleimkuhler authored and jijeesh committed Mar 23, 2021
1 parent 33aea65 commit 026704c
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 199 deletions.
30 changes: 3 additions & 27 deletions cli/cmd/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"os"
"time"

pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
"github.com/linkerd/linkerd2/pkg/healthcheck"
Expand All @@ -20,10 +19,7 @@ type profileOptions struct {
template bool
openAPI string
proto string
tap string
ignoreCluster bool
tapDuration time.Duration
tapRouteLimit uint
}

func newProfileOptions() *profileOptions {
Expand All @@ -32,10 +28,7 @@ func newProfileOptions() *profileOptions {
template: false,
openAPI: "",
proto: "",
tap: "",
ignoreCluster: false,
tapDuration: 5 * time.Second,
tapRouteLimit: 20,
}
}

Expand All @@ -50,17 +43,10 @@ func (options *profileOptions) validate() error {
if options.proto != "" {
outputs++
}
if options.tap != "" {
outputs++
}
if outputs != 1 {
return errors.New("You must specify exactly one of --template or --open-api or --proto or --tap")
return errors.New("You must specify exactly one of --template or --open-api or --proto")
}

// service profile generation based on tap data requires access to k8s cluster
if options.ignoreCluster && options.tap != "" {
return errors.New("--ignore-cluster and --tap flags are mutually exclusive; SP generation based on tap data requires access-check to k8s cluster")
}
// a DNS-1035 label must consist of lower case alphanumeric characters or '-',
// start with an alphabetic character, and end with an alphanumeric character
if errs := validation.IsDNS1035Label(options.name); len(errs) != 0 {
Expand All @@ -82,7 +68,7 @@ func newCmdProfile() *cobra.Command {
options := newProfileOptions()

cmd := &cobra.Command{
Use: "profile [flags] (--template | --open-api file | --proto file | --tap resource) (SERVICE)",
Use: "profile [flags] (--template | --open-api file | --proto file) (SERVICE)",
Short: "Output service profile config for Kubernetes",
Long: "Output service profile config for Kubernetes.",
Example: ` # Output a basic template to apply after modification.
Expand All @@ -93,9 +79,6 @@ func newCmdProfile() *cobra.Command {
# Generate a profile from a protobuf definition.
linkerd profile -n emojivoto --proto Voting.proto vote-svc
# Generate a profile by watching live traffic based off tap data.
linkerd profile -n emojivoto web-svc --tap deploy/web --tap-duration 10s --tap-route-limit 5
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -104,18 +87,16 @@ func newCmdProfile() *cobra.Command {
}
options.name = args[0]
clusterDomain := defaultClusterDomain
var k8sAPI *k8s.KubernetesAPI

err := options.validate()
if err != nil {
return err
}
// performs an online profile generation and access-check to k8s cluster to extract
// clusterDomain from linkerd configuration
// profile generation based on tap data requires access to k8s cluster
if !options.ignoreCluster {
var err error
k8sAPI, err = k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)

if err != nil {
return err
Expand All @@ -135,8 +116,6 @@ func newCmdProfile() *cobra.Command {
return profiles.RenderProfileTemplate(options.namespace, options.name, clusterDomain, os.Stdout)
} else if options.openAPI != "" {
return profiles.RenderOpenAPI(options.openAPI, options.namespace, options.name, clusterDomain, os.Stdout)
} else if options.tap != "" {
return profiles.RenderTapOutputProfile(cmd.Context(), k8sAPI, options.tap, options.namespace, options.name, clusterDomain, options.tapDuration, int(options.tapRouteLimit), os.Stdout)
} else if options.proto != "" {
return profiles.RenderProto(options.proto, options.namespace, options.name, clusterDomain, os.Stdout)
}
Expand All @@ -148,9 +127,6 @@ func newCmdProfile() *cobra.Command {

cmd.PersistentFlags().BoolVar(&options.template, "template", options.template, "Output a service profile template")
cmd.PersistentFlags().StringVar(&options.openAPI, "open-api", options.openAPI, "Output a service profile based on the given OpenAPI spec file")
cmd.PersistentFlags().StringVar(&options.tap, "tap", options.tap, "Output a service profile based on tap data for the given target resource")
cmd.PersistentFlags().DurationVar(&options.tapDuration, "tap-duration", options.tapDuration, "Duration over which tap data is collected (for example: \"10s\", \"1m\", \"10m\")")
cmd.PersistentFlags().UintVar(&options.tapRouteLimit, "tap-route-limit", options.tapRouteLimit, "Max number of routes to add to the profile")
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
cmd.PersistentFlags().StringVar(&options.proto, "proto", options.proto, "Output a service profile based on the given Protobuf spec file")
cmd.PersistentFlags().BoolVar(&options.ignoreCluster, "ignore-cluster", options.ignoreCluster, "Output a service profile through offline generation")
Expand Down
4 changes: 2 additions & 2 deletions cli/cmd/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestParseProfile(t *testing.T) {

func TestValidateOptions(t *testing.T) {
options := newProfileOptions()
exp := errors.New("You must specify exactly one of --template or --open-api or --proto or --tap")
exp := errors.New("You must specify exactly one of --template or --open-api or --proto")
err := options.validate()
if err == nil || err.Error() != exp.Error() {
t.Fatalf("validateOptions returned unexpected error: %s (expected: %s) for options: %+v", err, exp, options)
Expand All @@ -44,7 +44,7 @@ func TestValidateOptions(t *testing.T) {
options = newProfileOptions()
options.template = true
options.openAPI = "openAPI"
exp = errors.New("You must specify exactly one of --template or --open-api or --proto or --tap")
exp = errors.New("You must specify exactly one of --template or --open-api or --proto")
err = options.validate()
if err == nil || err.Error() != exp.Error() {
t.Fatalf("validateOptions returned unexpected error: %s (expected: %s) for options: %+v", err, exp, options)
Expand Down
29 changes: 11 additions & 18 deletions pkg/profiles/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io/ioutil"
"net/http"
"path"
"regexp"
"sort"

"github.com/go-openapi/spec"
Expand All @@ -20,8 +19,6 @@ const (
xLinkerdTimeout = "x-linkerd-timeout"
)

var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)

// RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
// ServiceProfile to a buffer, given a namespace, service, and control plane
// namespace.
Expand Down Expand Up @@ -58,7 +55,7 @@ func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomai
Name: fmt.Sprintf("%s.%s.svc.%s", name, namespace, clusterDomain),
Namespace: namespace,
},
TypeMeta: serviceProfileMeta,
TypeMeta: ServiceProfileMeta,
}

routes := make([]*sp.RouteSpec, 0)
Expand All @@ -74,33 +71,33 @@ func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomai
for _, relPath := range paths {
item := swagger.Paths.Paths[relPath]
path := path.Join(swagger.BasePath, relPath)
pathRegex := pathToRegex(path)
pathRegex := PathToRegex(path)
if item.Delete != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodDelete, item.Delete)
spec := MkRouteSpec(path, pathRegex, http.MethodDelete, item.Delete)
routes = append(routes, spec)
}
if item.Get != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodGet, item.Get)
spec := MkRouteSpec(path, pathRegex, http.MethodGet, item.Get)
routes = append(routes, spec)
}
if item.Head != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodHead, item.Head)
spec := MkRouteSpec(path, pathRegex, http.MethodHead, item.Head)
routes = append(routes, spec)
}
if item.Options != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodOptions, item.Options)
spec := MkRouteSpec(path, pathRegex, http.MethodOptions, item.Options)
routes = append(routes, spec)
}
if item.Patch != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPatch, item.Patch)
spec := MkRouteSpec(path, pathRegex, http.MethodPatch, item.Patch)
routes = append(routes, spec)
}
if item.Post != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPost, item.Post)
spec := MkRouteSpec(path, pathRegex, http.MethodPost, item.Post)
routes = append(routes, spec)
}
if item.Put != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPut, item.Put)
spec := MkRouteSpec(path, pathRegex, http.MethodPut, item.Put)
routes = append(routes, spec)
}
}
Expand All @@ -109,7 +106,8 @@ func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomai
return profile
}

func mkRouteSpec(path, pathRegex string, method string, operation *spec.Operation) *sp.RouteSpec {
// MkRouteSpec makes a service profile route from an OpenAPI operation.
func MkRouteSpec(path, pathRegex string, method string, operation *spec.Operation) *sp.RouteSpec {
retryable := false
timeout := ""
var responses *spec.Responses
Expand All @@ -127,11 +125,6 @@ func mkRouteSpec(path, pathRegex string, method string, operation *spec.Operatio
}
}

func pathToRegex(path string) string {
escaped := regexp.QuoteMeta(path)
return pathParamRegex.ReplaceAllLiteralString(escaped, "[^/]*")
}

func toReqMatch(path string, method string) *sp.RequestMatch {
return &sp.RequestMatch{
PathRegex: path,
Expand Down
2 changes: 1 addition & 1 deletion pkg/profiles/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestSwaggerToServiceProfile(t *testing.T) {
}

expectedServiceProfile := sp.ServiceProfile{
TypeMeta: serviceProfileMeta,
TypeMeta: ServiceProfileMeta,
ObjectMeta: metav1.ObjectMeta{
Name: name + "." + namespace + ".svc." + clusterDomain,
Namespace: namespace,
Expand Down
13 changes: 11 additions & 2 deletions pkg/profiles/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"regexp"
"text/template"
"time"

Expand All @@ -16,15 +17,17 @@ import (
"sigs.k8s.io/yaml"
)

var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)

type profileTemplateConfig struct {
ServiceNamespace string
ServiceName string
ClusterDomain string
}

var (
// serviceProfileMeta is the TypeMeta for the ServiceProfile custom resource.
serviceProfileMeta = metav1.TypeMeta{
// ServiceProfileMeta is the TypeMeta for the ServiceProfile custom resource.
ServiceProfileMeta = metav1.TypeMeta{
APIVersion: k8s.ServiceProfileAPIVersion,
Kind: k8s.ServiceProfileKind,
}
Expand Down Expand Up @@ -241,3 +244,9 @@ func writeProfile(profile sp.ServiceProfile, w io.Writer) error {
_, err = w.Write(output)
return err
}

// PathToRegex converts a path into a regex.
func PathToRegex(path string) string {
escaped := regexp.QuoteMeta(path)
return pathParamRegex.ReplaceAllLiteralString(escaped, "[^/]*")
}
2 changes: 1 addition & 1 deletion pkg/profiles/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain
Name: fmt.Sprintf("%s.%s.svc.%s", name, namespace, clusterDomain),
Namespace: namespace,
},
TypeMeta: serviceProfileMeta,
TypeMeta: ServiceProfileMeta,
Spec: sp.ServiceProfileSpec{
Routes: routes,
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/profiles/proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ service VotingService {
parser := proto.NewParser(strings.NewReader(protobuf))

expectedServiceProfile := sp.ServiceProfile{
TypeMeta: serviceProfileMeta,
TypeMeta: ServiceProfileMeta,
ObjectMeta: metav1.ObjectMeta{
Name: name + "." + namespace + ".svc." + clusterDomain,
Namespace: namespace,
Expand Down
Loading

0 comments on commit 026704c

Please sign in to comment.