1 package cmd
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9
10 sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
11 pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
12 "github.com/linkerd/linkerd2/pkg/healthcheck"
13 "github.com/linkerd/linkerd2/pkg/k8s"
14 "github.com/linkerd/linkerd2/pkg/profiles"
15 "github.com/spf13/cobra"
16 "k8s.io/apimachinery/pkg/util/validation"
17 "sigs.k8s.io/yaml"
18 )
19
20 type profileOptions struct {
21 name string
22 namespace string
23 template bool
24 openAPI string
25 proto string
26 ignoreCluster bool
27 output string
28 }
29
30 func newProfileOptions() *profileOptions {
31 return &profileOptions{
32 name: "",
33 template: false,
34 openAPI: "",
35 proto: "",
36 ignoreCluster: false,
37 output: "yaml",
38 }
39 }
40
41 func (options *profileOptions) validate() error {
42 outputs := 0
43 if options.template {
44 outputs++
45 }
46 if options.openAPI != "" {
47 outputs++
48 }
49 if options.proto != "" {
50 outputs++
51 }
52 if outputs != 1 {
53 return errors.New("You must specify exactly one of --template or --open-api or --proto")
54 }
55
56
57
58 if errs := validation.IsDNS1035Label(options.name); len(errs) != 0 {
59 return fmt.Errorf("invalid service %q: %v", options.name, errs)
60 }
61
62
63
64 if errs := validation.IsDNS1123Label(options.namespace); len(errs) != 0 {
65 return fmt.Errorf("invalid namespace %q: %v", options.namespace, errs)
66 }
67
68 return nil
69 }
70
71
72
73 func newCmdProfile() *cobra.Command {
74 options := newProfileOptions()
75
76 cmd := &cobra.Command{
77 Use: "profile [flags] (--template | --open-api file | --proto file) (SERVICE)",
78 Short: "Output service profile config for Kubernetes",
79 Long: "Output service profile config for Kubernetes.",
80 Example: ` # Output a basic template to apply after modification.
81 linkerd profile -n emojivoto --template web-svc
82
83 # Generate a profile from an OpenAPI specification.
84 linkerd profile -n emojivoto --open-api web-svc.swagger web-svc
85
86 # Generate a profile from a protobuf definition.
87 linkerd profile -n emojivoto --proto Voting.proto vote-svc
88 `,
89 Args: cobra.ExactArgs(1),
90 RunE: func(cmd *cobra.Command, args []string) error {
91 if options.namespace == "" {
92 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
93 }
94 options.name = args[0]
95 clusterDomain := defaultClusterDomain
96
97 err := options.validate()
98 if err != nil {
99 return err
100 }
101
102
103 if !options.ignoreCluster {
104 var err error
105 k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
106
107 if err != nil {
108 return err
109 }
110
111 _, values, err := healthcheck.FetchCurrentConfiguration(cmd.Context(), k8sAPI, controlPlaneNamespace)
112 if err != nil {
113 return err
114 }
115
116 if cd := values.ClusterDomain; cd != "" {
117 clusterDomain = cd
118 }
119 }
120
121 var profile *sp.ServiceProfile
122 if options.template {
123 return profiles.RenderProfileTemplate(options.namespace, options.name, clusterDomain, os.Stdout, options.output)
124 } else if options.openAPI != "" {
125 profile, err = profiles.RenderOpenAPI(options.openAPI, options.namespace, options.name, clusterDomain)
126 } else if options.proto != "" {
127 profile, err = profiles.RenderProto(options.proto, options.namespace, options.name, clusterDomain)
128 } else {
129 return errors.New("one of --template, --open-api, or --proto must be specified")
130 }
131 if err != nil {
132 return err
133 }
134
135 return writeProfile(profile, os.Stdout, options.output)
136 },
137 }
138
139 cmd.PersistentFlags().BoolVar(&options.template, "template", options.template, "Output a service profile template")
140 cmd.PersistentFlags().StringVar(&options.openAPI, "open-api", options.openAPI, "Output a service profile based on the given OpenAPI spec file")
141 cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
142 cmd.PersistentFlags().StringVar(&options.proto, "proto", options.proto, "Output a service profile based on the given Protobuf spec file")
143 cmd.PersistentFlags().BoolVar(&options.ignoreCluster, "ignore-cluster", options.ignoreCluster, "Output a service profile through offline generation")
144 cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: yaml, json")
145 return cmd
146 }
147
148 func writeProfile(profile *sp.ServiceProfile, w io.Writer, format string) error {
149 var output []byte
150 var err error
151 if format == "yaml" {
152 output, err = yaml.Marshal(profile)
153 } else if format == "json" {
154 output, err = json.Marshal(profile)
155 } else {
156 return fmt.Errorf("unknown output format: %s", format)
157 }
158 if err != nil {
159 return fmt.Errorf("Error writing Service Profile: %w", err)
160 }
161 _, err = w.Write(output)
162 return err
163 }
164
View as plain text