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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
|
||||||
pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
|
pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
|
||||||
"github.com/linkerd/linkerd2/pkg/healthcheck"
|
"github.com/linkerd/linkerd2/pkg/healthcheck"
|
||||||
"github.com/linkerd/linkerd2/pkg/k8s"
|
"github.com/linkerd/linkerd2/pkg/k8s"
|
||||||
"github.com/linkerd/linkerd2/pkg/profiles"
|
"github.com/linkerd/linkerd2/pkg/profiles"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/util/validation"
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type profileOptions struct {
|
type profileOptions struct {
|
||||||
|
|
@ -20,6 +24,7 @@ type profileOptions struct {
|
||||||
openAPI string
|
openAPI string
|
||||||
proto string
|
proto string
|
||||||
ignoreCluster bool
|
ignoreCluster bool
|
||||||
|
output string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProfileOptions() *profileOptions {
|
func newProfileOptions() *profileOptions {
|
||||||
|
|
@ -29,6 +34,7 @@ func newProfileOptions() *profileOptions {
|
||||||
openAPI: "",
|
openAPI: "",
|
||||||
proto: "",
|
proto: "",
|
||||||
ignoreCluster: false,
|
ignoreCluster: false,
|
||||||
|
output: "yaml",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,16 +118,21 @@ func newCmdProfile() *cobra.Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var profile *sp.ServiceProfile
|
||||||
if options.template {
|
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 != "" {
|
} 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 != "" {
|
} 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 writeProfile(profile, os.Stdout, options.output)
|
||||||
return errors.New("Unexpected error")
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,6 +141,23 @@ func newCmdProfile() *cobra.Command {
|
||||||
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
|
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().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().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
|
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) {
|
func TestParseProfile(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
err := profiles.RenderProfileTemplate("myns", "mysvc", "mycluster.local", &buf)
|
err := profiles.RenderProfileTemplate("myns", "mysvc", "mycluster.local", &buf, "yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error rendering service profile template: %v", err)
|
t.Fatalf("Error rendering service profile template: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,31 +21,31 @@ const (
|
||||||
// RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
|
// RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
|
||||||
// ServiceProfile to a buffer, given a namespace, service, and control plane
|
// ServiceProfile to a buffer, given a namespace, service, and control plane
|
||||||
// namespace.
|
// 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)
|
input, err := readFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, err := io.ReadAll(input)
|
bytes, err := io.ReadAll(input)
|
||||||
if err != nil {
|
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)
|
json, err := yaml.YAMLToJSON(bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error parsing yaml: %w", err)
|
return nil, fmt.Errorf("Error parsing yaml: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
swagger := spec.Swagger{}
|
swagger := spec.Swagger{}
|
||||||
err = swagger.UnmarshalJSON(json)
|
err = swagger.UnmarshalJSON(json)
|
||||||
if err != nil {
|
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)
|
profile := swaggerToServiceProfile(swagger, namespace, name, clusterDomain)
|
||||||
|
|
||||||
return writeProfile(profile, w)
|
return &profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomain string) sp.ServiceProfile {
|
func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomain string) sp.ServiceProfile {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/linkerd/linkerd2/pkg/k8s"
|
"github.com/linkerd/linkerd2/pkg/k8s"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/validation"
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
|
||||||
"sigs.k8s.io/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
|
// RenderProfileTemplate renders a ServiceProfile template to a buffer, given a
|
||||||
// namespace, service, and control plane namespace.
|
// 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)
|
config := buildConfig(namespace, service, clusterDomain)
|
||||||
template, err := template.New("profile").Parse(Template)
|
template, err := template.New("profile").Parse(Template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -222,8 +223,20 @@ func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer
|
||||||
return err
|
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())
|
_, err = w.Write(buf.Bytes())
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unknown output format: %s", format)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFile(fileName string) (io.Reader, error) {
|
func readFile(fileName string) (io.Reader, error) {
|
||||||
|
|
@ -233,15 +246,6 @@ func readFile(fileName string) (io.Reader, error) {
|
||||||
return os.Open(filepath.Clean(fileName))
|
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.
|
// PathToRegex converts a path into a regex.
|
||||||
func PathToRegex(path string) string {
|
func PathToRegex(path string) string {
|
||||||
escaped := regexp.QuoteMeta(path)
|
escaped := regexp.QuoteMeta(path)
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,9 @@ func FuzzRenderProto(data []byte) int {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
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 {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
defer w.Close()
|
|
||||||
_ = RenderProto(protofile.Name(), namespace, name, clusterDomain, w)
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package profiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -15,20 +14,15 @@ import (
|
||||||
// RenderProto reads a protobuf definition file and renders the corresponding
|
// RenderProto reads a protobuf definition file and renders the corresponding
|
||||||
// ServiceProfile to a buffer, given a namespace, service, and control plane
|
// ServiceProfile to a buffer, given a namespace, service, and control plane
|
||||||
// namespace.
|
// 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)
|
input, err := readFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := proto.NewParser(input)
|
parser := proto.NewParser(input)
|
||||||
|
|
||||||
profile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
|
return protoToServiceProfile(parser, namespace, name, clusterDomain)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return writeProfile(*profile, w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
|
func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -36,12 +37,14 @@ type profileOptions struct {
|
||||||
tap string
|
tap string
|
||||||
tapDuration time.Duration
|
tapDuration time.Duration
|
||||||
tapRouteLimit uint
|
tapRouteLimit uint
|
||||||
|
output string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProfileOptions() *profileOptions {
|
func newProfileOptions() *profileOptions {
|
||||||
return &profileOptions{
|
return &profileOptions{
|
||||||
tapDuration: 5 * time.Second,
|
tapDuration: 5 * time.Second,
|
||||||
tapRouteLimit: 20,
|
tapRouteLimit: 20,
|
||||||
|
output: "yaml",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,13 +139,14 @@ func newCmdProfile() *cobra.Command {
|
||||||
if cd := values.ClusterDomain; cd != "" {
|
if cd := values.ClusterDomain; cd != "" {
|
||||||
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().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().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().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.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(
|
pkgcmd.ConfigureNamespaceFlagCompletion(
|
||||||
cmd, []string{"namespace"},
|
cmd, []string{"namespace"},
|
||||||
|
|
@ -153,7 +157,7 @@ func newCmdProfile() *cobra.Command {
|
||||||
// renderTapOutputProfile performs a tap on the desired resource and generates
|
// renderTapOutputProfile performs a tap on the desired resource and generates
|
||||||
// a service profile with routes pre-populated from the tap data
|
// a service profile with routes pre-populated from the tap data
|
||||||
// Only inbound tap traffic is considered.
|
// 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{
|
requestParams := pkg.TapRequestParams{
|
||||||
Resource: tapResource,
|
Resource: tapResource,
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
|
|
@ -167,7 +171,14 @@ func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapR
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("Error writing Service Profile: %w", err)
|
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{}
|
profileYaml := &bytes.Buffer{}
|
||||||
err := profiles.RenderProfileTemplate(namespace, service, h.clusterDomain, profileYaml)
|
err := profiles.RenderProfileTemplate(namespace, service, h.clusterDomain, profileYaml, "yaml")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue