linkerd2/cli/cmd/profile.go

261 lines
6.8 KiB
Go

package cmd
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"regexp"
"sort"
"github.com/ghodss/yaml"
"github.com/go-openapi/spec"
sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
"github.com/linkerd/linkerd2/pkg/profiles"
"github.com/spf13/cobra"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
)
type templateConfig struct {
ControlPlaneNamespace string
ServiceNamespace string
ServiceName string
ClusterZone string
}
var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)
type profileOptions struct {
name string
namespace string
template bool
openAPI string
}
func newProfileOptions() *profileOptions {
return &profileOptions{
name: "",
namespace: "default",
template: false,
openAPI: "",
}
}
func (options *profileOptions) validate() error {
outputs := 0
if options.template {
outputs++
}
if options.openAPI != "" {
outputs++
}
if outputs != 1 {
return errors.New("You must specify exactly one of --template or --open-api")
}
// 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 {
return fmt.Errorf("invalid service %q: %v", options.name, errs)
}
// a DNS-1123 label must consist of lower case alphanumeric characters or '-',
// and must start and end with an alphanumeric character
if errs := validation.IsDNS1123Label(options.namespace); len(errs) != 0 {
return fmt.Errorf("invalid namespace %q: %v", options.namespace, errs)
}
return nil
}
func newCmdProfile() *cobra.Command {
options := newProfileOptions()
cmd := &cobra.Command{
Use: "profile [flags] (--template | --open-api file) (SERVICE)",
Short: "Output service profile config for Kubernetes",
Long: `Output service profile config for Kubernetes.
This outputs a service profile for the given service.
If the --template flag is specified, it outputs a service profile template.
Edit the template and then apply it with kubectl to add a service profile to
a service.
Example:
linkerd profile -n emojivoto --template web-svc > web-svc-profile.yaml
# (edit web-svc-profile.yaml manually)
kubectl apply -f web-svc-profile.yaml
If the --open-api flag is specified, it reads the given OpenAPI
specification file and outputs a corresponding service profile.
Example:
linkerd profile -n emojivoto --open-api web-svc.swagger web-svc | kubectl apply -f -`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.name = args[0]
err := options.validate()
if err != nil {
return err
}
if options.template {
return profiles.RenderProfileTemplate(options.namespace, options.name, controlPlaneNamespace, os.Stdout)
} else if options.openAPI != "" {
return renderOpenAPI(options, os.Stdout)
}
// we should never get here
return errors.New("Unexpected error")
},
}
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().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
return cmd
}
func renderOpenAPI(options *profileOptions, w io.Writer) error {
var input io.Reader
if options.openAPI == "-" {
input = os.Stdin
} else {
var err error
input, err = os.Open(options.openAPI)
if err != nil {
return err
}
}
bytes, err := ioutil.ReadAll(input)
if err != nil {
return fmt.Errorf("Error reading file: %s", err)
}
json, err := yaml.YAMLToJSON(bytes)
if err != nil {
return fmt.Errorf("Error parsing yaml: %s", err)
}
swagger := spec.Swagger{}
err = swagger.UnmarshalJSON(json)
if err != nil {
return fmt.Errorf("Error parsing OpenAPI spec: %s", err)
}
profile := sp.ServiceProfile{
ObjectMeta: meta_v1.ObjectMeta{
Name: fmt.Sprintf("%s.%s.svc.cluster.local", options.name, options.namespace),
Namespace: controlPlaneNamespace,
},
TypeMeta: meta_v1.TypeMeta{
APIVersion: "linkerd.io/v1alpha1",
Kind: "ServiceProfile",
},
}
routes := make([]*sp.RouteSpec, 0)
paths := make([]string, 0)
for path := range swagger.Paths.Paths {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
item := swagger.Paths.Paths[path]
pathRegex := pathToRegex(path)
if item.Delete != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodDelete, item.Delete.Responses)
routes = append(routes, spec)
}
if item.Get != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodGet, item.Get.Responses)
routes = append(routes, spec)
}
if item.Head != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodHead, item.Head.Responses)
routes = append(routes, spec)
}
if item.Options != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodOptions, item.Options.Responses)
routes = append(routes, spec)
}
if item.Patch != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPatch, item.Patch.Responses)
routes = append(routes, spec)
}
if item.Post != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPost, item.Post.Responses)
routes = append(routes, spec)
}
if item.Put != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPut, item.Put.Responses)
routes = append(routes, spec)
}
}
profile.Spec.Routes = routes
output, err := yaml.Marshal(profile)
if err != nil {
return fmt.Errorf("Error writing Service Profile: %s", err)
}
w.Write(output)
return nil
}
func mkRouteSpec(path, pathRegex string, method string, responses *spec.Responses) *sp.RouteSpec {
return &sp.RouteSpec{
Name: fmt.Sprintf("%s %s", method, path),
Condition: toReqMatch(pathRegex, method),
ResponseClasses: toRspClasses(responses),
}
}
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,
Method: method,
}
}
func toRspClasses(responses *spec.Responses) []*sp.ResponseClass {
if responses == nil {
return nil
}
classes := make([]*sp.ResponseClass, 0)
statuses := make([]int, 0)
for status := range responses.StatusCodeResponses {
statuses = append(statuses, status)
}
sort.Ints(statuses)
for _, status := range statuses {
cond := &sp.ResponseMatch{
Status: &sp.Range{
Min: uint32(status),
Max: uint32(status),
},
}
classes = append(classes, &sp.ResponseClass{
Condition: cond,
IsFailure: status >= 500,
})
}
return classes
}