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