...

Source file src/github.com/linkerd/linkerd2/cli/cmd/profile.go

Documentation: github.com/linkerd/linkerd2/cli/cmd

     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  	// a DNS-1035 label must consist of lower case alphanumeric characters or '-',
    57  	// start with an alphabetic character, and end with an alphanumeric character
    58  	if errs := validation.IsDNS1035Label(options.name); len(errs) != 0 {
    59  		return fmt.Errorf("invalid service %q: %v", options.name, errs)
    60  	}
    61  
    62  	// a DNS-1123 label must consist of lower case alphanumeric characters or '-',
    63  	// and must start and end with an alphanumeric character
    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  // newCmdProfile creates a new cobra command for the Profile subcommand which
    72  // generates Linkerd service profiles.
    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  			// performs an online profile generation and access-check to k8s cluster to extract
   102  			// clusterDomain from linkerd configuration
   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