mirror of https://github.com/linkerd/linkerd2.git
				
				
				
			Add json output format support to linkerd profile command (#12611)
Add an`-o/--output` flag to the `linkerd profile` command which outputs ServiceProfile manifests. The supported output formats are yaml (default) and json. Both of these formats are supported by kubectl and either one can be piped into `kubectl apply`. Signed-off-by: Alex Leong <alex@buoyant.io>
This commit is contained in:
		
							parent
							
								
									10b1a7af6a
								
							
						
					
					
						commit
						5a67e83ff5
					
				| 
						 | 
				
			
			@ -1,16 +1,20 @@
 | 
			
		|||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 | 
			
		||||
	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 | 
			
		||||
	"github.com/linkerd/linkerd2/pkg/healthcheck"
 | 
			
		||||
	"github.com/linkerd/linkerd2/pkg/k8s"
 | 
			
		||||
	"github.com/linkerd/linkerd2/pkg/profiles"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/validation"
 | 
			
		||||
	"sigs.k8s.io/yaml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type profileOptions struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +24,7 @@ type profileOptions struct {
 | 
			
		|||
	openAPI       string
 | 
			
		||||
	proto         string
 | 
			
		||||
	ignoreCluster bool
 | 
			
		||||
	output        string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newProfileOptions() *profileOptions {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +34,7 @@ func newProfileOptions() *profileOptions {
 | 
			
		|||
		openAPI:       "",
 | 
			
		||||
		proto:         "",
 | 
			
		||||
		ignoreCluster: false,
 | 
			
		||||
		output:        "yaml",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,16 +118,21 @@ func newCmdProfile() *cobra.Command {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var profile *sp.ServiceProfile
 | 
			
		||||
			if options.template {
 | 
			
		||||
				return profiles.RenderProfileTemplate(options.namespace, options.name, clusterDomain, os.Stdout)
 | 
			
		||||
				return profiles.RenderProfileTemplate(options.namespace, options.name, clusterDomain, os.Stdout, options.output)
 | 
			
		||||
			} else if options.openAPI != "" {
 | 
			
		||||
				return profiles.RenderOpenAPI(options.openAPI, options.namespace, options.name, clusterDomain, os.Stdout)
 | 
			
		||||
				profile, err = profiles.RenderOpenAPI(options.openAPI, options.namespace, options.name, clusterDomain)
 | 
			
		||||
			} else if options.proto != "" {
 | 
			
		||||
				return profiles.RenderProto(options.proto, options.namespace, options.name, clusterDomain, os.Stdout)
 | 
			
		||||
				profile, err = profiles.RenderProto(options.proto, options.namespace, options.name, clusterDomain)
 | 
			
		||||
			} else {
 | 
			
		||||
				return errors.New("one of --template, --open-api, or --proto must be specified")
 | 
			
		||||
			}
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// we should never get here
 | 
			
		||||
			return errors.New("Unexpected error")
 | 
			
		||||
			return writeProfile(profile, os.Stdout, options.output)
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +141,23 @@ func newCmdProfile() *cobra.Command {
 | 
			
		|||
	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")
 | 
			
		||||
 | 
			
		||||
	cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: yaml, json")
 | 
			
		||||
	return cmd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeProfile(profile *sp.ServiceProfile, w io.Writer, format string) error {
 | 
			
		||||
	var output []byte
 | 
			
		||||
	var err error
 | 
			
		||||
	if format == "yaml" {
 | 
			
		||||
		output, err = yaml.Marshal(profile)
 | 
			
		||||
	} else if format == "json" {
 | 
			
		||||
		output, err = json.Marshal(profile)
 | 
			
		||||
	} else {
 | 
			
		||||
		return fmt.Errorf("unknown output format: %s", format)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Error writing Service Profile: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	_, err = w.Write(output)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ import (
 | 
			
		|||
func TestParseProfile(t *testing.T) {
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	err := profiles.RenderProfileTemplate("myns", "mysvc", "mycluster.local", &buf)
 | 
			
		||||
	err := profiles.RenderProfileTemplate("myns", "mysvc", "mycluster.local", &buf, "yaml")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Error rendering service profile template: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,31 +21,31 @@ const (
 | 
			
		|||
// RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
 | 
			
		||||
// ServiceProfile to a buffer, given a namespace, service, and control plane
 | 
			
		||||
// namespace.
 | 
			
		||||
func RenderOpenAPI(fileName, namespace, name, clusterDomain string, w io.Writer) error {
 | 
			
		||||
func RenderOpenAPI(fileName, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
 | 
			
		||||
 | 
			
		||||
	input, err := readFile(fileName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bytes, err := io.ReadAll(input)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Error reading file: %w", err)
 | 
			
		||||
		return nil, fmt.Errorf("Error reading file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	json, err := yaml.YAMLToJSON(bytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Error parsing yaml: %w", err)
 | 
			
		||||
		return nil, fmt.Errorf("Error parsing yaml: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	swagger := spec.Swagger{}
 | 
			
		||||
	err = swagger.UnmarshalJSON(json)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Error parsing OpenAPI spec: %w", err)
 | 
			
		||||
		return nil, fmt.Errorf("Error parsing OpenAPI spec: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	profile := swaggerToServiceProfile(swagger, namespace, name, clusterDomain)
 | 
			
		||||
 | 
			
		||||
	return writeProfile(profile, w)
 | 
			
		||||
	return &profile, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomain string) sp.ServiceProfile {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ import (
 | 
			
		|||
	"github.com/linkerd/linkerd2/pkg/k8s"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/validation"
 | 
			
		||||
	yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
 | 
			
		||||
	"sigs.k8s.io/yaml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -210,7 +211,7 @@ func buildConfig(namespace, service, clusterDomain string) *profileTemplateConfi
 | 
			
		|||
 | 
			
		||||
// RenderProfileTemplate renders a ServiceProfile template to a buffer, given a
 | 
			
		||||
// namespace, service, and control plane namespace.
 | 
			
		||||
func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer) error {
 | 
			
		||||
func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer, format string) error {
 | 
			
		||||
	config := buildConfig(namespace, service, clusterDomain)
 | 
			
		||||
	template, err := template.New("profile").Parse(Template)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -222,8 +223,20 @@ func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer
 | 
			
		|||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = w.Write(buf.Bytes())
 | 
			
		||||
	return err
 | 
			
		||||
	if format == "json" {
 | 
			
		||||
		bytes, err := yamlDecoder.ToJSON(buf.Bytes())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		_, err = w.Write(append(bytes, '\n'))
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if format == "yaml" {
 | 
			
		||||
		_, err = w.Write(buf.Bytes())
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("unknown output format: %s", format)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readFile(fileName string) (io.Reader, error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -233,15 +246,6 @@ func readFile(fileName string) (io.Reader, error) {
 | 
			
		|||
	return os.Open(filepath.Clean(fileName))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeProfile(profile sp.ServiceProfile, w io.Writer) error {
 | 
			
		||||
	output, err := yaml.Marshal(profile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Error writing Service Profile: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	_, err = w.Write(output)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PathToRegex converts a path into a regex.
 | 
			
		||||
func PathToRegex(path string) string {
 | 
			
		||||
	escaped := regexp.QuoteMeta(path)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,11 +42,9 @@ func FuzzRenderProto(data []byte) int {
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	w, err := os.OpenFile("/dev/null", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
 | 
			
		||||
	_, err = RenderProto(protofile.Name(), namespace, name, clusterDomain)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	defer w.Close()
 | 
			
		||||
	_ = RenderProto(protofile.Name(), namespace, name, clusterDomain, w)
 | 
			
		||||
	return 1
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ package profiles
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,20 +14,15 @@ import (
 | 
			
		|||
// RenderProto reads a protobuf definition file and renders the corresponding
 | 
			
		||||
// ServiceProfile to a buffer, given a namespace, service, and control plane
 | 
			
		||||
// namespace.
 | 
			
		||||
func RenderProto(fileName, namespace, name, clusterDomain string, w io.Writer) error {
 | 
			
		||||
func RenderProto(fileName, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
 | 
			
		||||
	input, err := readFile(fileName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := proto.NewParser(input)
 | 
			
		||||
 | 
			
		||||
	profile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return writeProfile(*profile, w)
 | 
			
		||||
	return protoToServiceProfile(parser, namespace, name, clusterDomain)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ package cmd
 | 
			
		|||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
| 
						 | 
				
			
			@ -36,12 +37,14 @@ type profileOptions struct {
 | 
			
		|||
	tap           string
 | 
			
		||||
	tapDuration   time.Duration
 | 
			
		||||
	tapRouteLimit uint
 | 
			
		||||
	output        string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newProfileOptions() *profileOptions {
 | 
			
		||||
	return &profileOptions{
 | 
			
		||||
		tapDuration:   5 * time.Second,
 | 
			
		||||
		tapRouteLimit: 20,
 | 
			
		||||
		output:        "yaml",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -136,13 +139,14 @@ func newCmdProfile() *cobra.Command {
 | 
			
		|||
			if cd := values.ClusterDomain; cd != "" {
 | 
			
		||||
				clusterDomain = cd
 | 
			
		||||
			}
 | 
			
		||||
			return renderTapOutputProfile(cmd.Context(), k8sAPI, options.tap, options.namespace, options.name, clusterDomain, options.tapDuration, int(options.tapRouteLimit), os.Stdout)
 | 
			
		||||
			return renderTapOutputProfile(cmd.Context(), k8sAPI, options.tap, options.namespace, options.name, clusterDomain, options.tapDuration, int(options.tapRouteLimit), options.output, os.Stdout)
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	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().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: yaml, json")
 | 
			
		||||
 | 
			
		||||
	pkgcmd.ConfigureNamespaceFlagCompletion(
 | 
			
		||||
		cmd, []string{"namespace"},
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +157,7 @@ func newCmdProfile() *cobra.Command {
 | 
			
		|||
// renderTapOutputProfile performs a tap on the desired resource and generates
 | 
			
		||||
// a service profile with routes pre-populated from the tap data
 | 
			
		||||
// Only inbound tap traffic is considered.
 | 
			
		||||
func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapResource, namespace, name, clusterDomain string, tapDuration time.Duration, routeLimit int, w io.Writer) error {
 | 
			
		||||
func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapResource, namespace, name, clusterDomain string, tapDuration time.Duration, routeLimit int, format string, w io.Writer) error {
 | 
			
		||||
	requestParams := pkg.TapRequestParams{
 | 
			
		||||
		Resource:  tapResource,
 | 
			
		||||
		Namespace: namespace,
 | 
			
		||||
| 
						 | 
				
			
			@ -167,7 +171,14 @@ func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapR
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	output, err := yaml.Marshal(profile)
 | 
			
		||||
	var output []byte
 | 
			
		||||
	if format == "yaml" {
 | 
			
		||||
		output, err = yaml.Marshal(profile)
 | 
			
		||||
	} else if format == "json" {
 | 
			
		||||
		output, err = json.Marshal(profile)
 | 
			
		||||
	} else {
 | 
			
		||||
		return errors.New("output format must be one of yaml or json")
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Error writing Service Profile: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,7 +74,7 @@ func (h *handler) handleProfileDownload(w http.ResponseWriter, req *http.Request
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	profileYaml := &bytes.Buffer{}
 | 
			
		||||
	err := profiles.RenderProfileTemplate(namespace, service, h.clusterDomain, profileYaml)
 | 
			
		||||
	err := profiles.RenderProfileTemplate(namespace, service, h.clusterDomain, profileYaml, "yaml")
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error(err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue