From b10eee87eea32780a65df741622ba1e20c65a306 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Thu, 25 Nov 2021 11:50:52 +0100 Subject: [PATCH] Simplify arguments of flux trace command It now accepts arguments in the forms / and instead of requiring api version and kind as flags. Signed-off-by: Jakob Schrettenbrunner --- cmd/flux/trace.go | 148 ++++++++++++++++++++++++++++++----------- cmd/flux/trace_test.go | 2 +- 2 files changed, 112 insertions(+), 38 deletions(-) diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go index 910c9d43..e04b3103 100644 --- a/cmd/flux/trace.go +++ b/cmd/flux/trace.go @@ -27,8 +27,10 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/flux2/internal/utils" @@ -39,20 +41,26 @@ import ( ) var traceCmd = &cobra.Command{ - Use: "trace [name]", - Short: "Trace an in-cluster object throughout the GitOps delivery pipeline", - Long: `The trace command shows how an object is managed by Flux, -from which source and revision it comes, and what's the latest reconciliation status.'`, + Use: "trace [ ...]", + Short: "Trace in-cluster objects throughout the GitOps delivery pipeline", + Long: `The trace command shows how one or more objects are managed by Flux, +from which source and revision they come, and what the latest reconciliation status is. + +You can also trace multiple objects with different resource kinds using / multiple times.`, Example: ` # Trace a Kubernetes Deployment - flux trace my-app --kind=deployment --api-version=apps/v1 --namespace=apps + flux trace -n apps deployment my-app - # Trace a Kubernetes Pod - flux trace redis-master-0 --kind=pod --api-version=v1 -n redis + # Trace a Kubernetes Pod and a config map + flux trace -n redis pod/redis-master-0 cm/redis # Trace a Kubernetes global object - flux trace redis --kind=namespace --api-version=v1 + flux trace namespace redis # Trace a Kubernetes custom resource + flux trace -n redis helmrelease redis + + # API Version and Kind can also be specified explicitly + # Note that either both, kind and api-version, or neither have to be specified. flux trace redis --kind=helmrelease --api-version=helm.toolkit.fluxcd.io/v2beta1 -n redis`, RunE: traceCmdRun, } @@ -73,19 +81,6 @@ func init() { } func traceCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("object name is required") - } - name := args[0] - - if traceArgs.kind == "" { - return fmt.Errorf("object kind is required (--kind)") - } - - if traceArgs.apiVersion == "" { - return fmt.Errorf("object apiVersion is required (--api-version)") - } - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() @@ -94,28 +89,35 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { return err } - gv, err := schema.ParseGroupVersion(traceArgs.apiVersion) + var objects []*unstructured.Unstructured + if traceArgs.kind != "" || traceArgs.apiVersion != "" { + var obj *unstructured.Unstructured + obj, err = getObjectStatic(ctx, kubeClient, args) + objects = []*unstructured.Unstructured{obj} + } else { + objects, err = getObjectDynamic(args) + } if err != nil { - return fmt.Errorf("invaild apiVersion: %w", err) + return err } - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(schema.GroupVersionKind{ - Group: gv.Group, - Version: gv.Version, - Kind: traceArgs.kind, - }) - - objName := types.NamespacedName{ - Namespace: *kubeconfigArgs.Namespace, - Name: name, - } + return traceObjects(ctx, kubeClient, objects) +} - err = kubeClient.Get(ctx, objName, obj) - if err != nil { - return fmt.Errorf("failed to find object: %w", err) +func traceObjects(ctx context.Context, kubeClient client.Client, objects []*unstructured.Unstructured) error { + for i, obj := range objects { + err := traceObject(ctx, kubeClient, obj) + if err != nil { + rootCmd.PrintErrf("failed to trace %v/%v in namespace %v: %v", obj.GetKind(), obj.GetName(), obj.GetNamespace(), err) + } + if i < len(objects)-1 { + rootCmd.Println("---") + } } + return nil +} +func traceObject(ctx context.Context, kubeClient client.Client, obj *unstructured.Unstructured) error { if ks, ok := isOwnerManagedByFlux(ctx, kubeClient, obj, kustomizev1.GroupVersion.Group); ok { report, err := traceKustomization(ctx, kubeClient, ks, obj) if err != nil { @@ -137,6 +139,78 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("object not managed by Flux") } +func getObjectStatic(ctx context.Context, kubeClient client.Client, args []string) (*unstructured.Unstructured, error) { + if len(args) < 1 { + return nil, fmt.Errorf("object name is required") + } + + if traceArgs.kind == "" { + return nil, fmt.Errorf("object kind is required (--kind)") + } + + if traceArgs.apiVersion == "" { + return nil, fmt.Errorf("object apiVersion is required (--api-version)") + } + + gv, err := schema.ParseGroupVersion(traceArgs.apiVersion) + if err != nil { + return nil, fmt.Errorf("invaild apiVersion: %w", err) + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: traceArgs.kind, + }) + + objName := types.NamespacedName{ + Namespace: *kubeconfigArgs.Namespace, + Name: args[0], + } + + if err = kubeClient.Get(ctx, objName, obj); err != nil { + return nil, fmt.Errorf("failed to find object: %w", err) + } + return obj, nil +} + +func getObjectDynamic(args []string) ([]*unstructured.Unstructured, error) { + r := resource.NewBuilder(kubeconfigArgs). + Unstructured(). + NamespaceParam(*kubeconfigArgs.Namespace).DefaultNamespace(). + ResourceTypeOrNameArgs(false, args...). + ContinueOnError(). + Latest(). + Do() + + if err := r.Err(); err != nil { + if resource.IsUsageError(err) { + return nil, fmt.Errorf("either `/` or ` ` is required as an argument") + } + return nil, err + } + + infos, err := r.Infos() + if err != nil { + return nil, fmt.Errorf("x: %v", err) + } + if len(infos) == 0 { + return nil, fmt.Errorf("failed to find object: %w", err) + } + + objects := []*unstructured.Unstructured{} + for _, info := range infos { + obj := &unstructured.Unstructured{} + obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) + if err != nil { + return objects, err + } + objects = append(objects, obj) + } + return objects, nil +} + func traceKustomization(ctx context.Context, kubeClient client.Client, ksName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { ks := &kustomizev1.Kustomization{} ksReady := &metav1.Condition{} diff --git a/cmd/flux/trace_test.go b/cmd/flux/trace_test.go index fc496de0..052bdbb3 100644 --- a/cmd/flux/trace_test.go +++ b/cmd/flux/trace_test.go @@ -10,7 +10,7 @@ import ( func TestTraceNoArgs(t *testing.T) { cmd := cmdTestCase{ args: "trace", - assert: assertError("object name is required"), + assert: assertError("either `/` or ` ` is required as an argument"), } cmd.runTestCmd(t) }