From 4305b8a77de4e77fac076b30a713e8cf20135fcf Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 23 Jun 2021 13:41:31 +0300 Subject: [PATCH 1/3] Implement flux trace command The trace command allows Flux users to point the CLI to a Kubernetes object in-cluster and get a detailed report about the GitOps pipeline that manages that particular object. Signed-off-by: Stefan Prodan --- cmd/flux/trace.go | 435 ++++++++++++++++++++++++++++++++++++++++ internal/utils/utils.go | 61 ++---- 2 files changed, 457 insertions(+), 39 deletions(-) create mode 100644 cmd/flux/trace.go diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go new file mode 100644 index 00000000..eee3da03 --- /dev/null +++ b/cmd/flux/trace.go @@ -0,0 +1,435 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "text/template" + + "github.com/spf13/cobra" + "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/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/flux2/internal/utils" + helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1" + fluxmeta "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" +) + +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.'`, + Example: ` # Trace a Kubernetes Deployment + flux trace my-app --kind=deployment --api-version=apps/v1 --namespace=apps`, + RunE: traceCmdRun, +} + +type traceFlags struct { + apiVersion string + kind string +} + +var traceArgs = traceFlags{} + +func init() { + traceCmd.Flags().StringVar(&traceArgs.kind, "kind", "", + "the Kubernetes object kind, e.g. Deployment'") + traceCmd.Flags().StringVar(&traceArgs.apiVersion, "api-version", "", + "the Kubernetes object API version, e.g. 'apps/v1'") + rootCmd.AddCommand(traceCmd) +} + +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() + + kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return err + } + + gv, err := schema.ParseGroupVersion(traceArgs.apiVersion) + if err != nil { + return 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: rootArgs.namespace, + Name: name, + } + + err = kubeClient.Get(ctx, objName, obj) + if err != nil { + return fmt.Errorf("failed to find object: %w", err) + } + + if ks, ok := isManagedByFlux(obj, kustomizev1.GroupVersion.Group); ok { + report, err := traceKustomization(ctx, kubeClient, ks, obj) + if err != nil { + return err + } + fmt.Println(report) + return nil + } + + if hr, ok := isManagedByFlux(obj, helmv2.GroupVersion.Group); ok { + report, err := traceHelm(ctx, kubeClient, hr, obj) + if err != nil { + return err + } + fmt.Println(report) + return nil + } + + return fmt.Errorf("object not managed by Flux") +} + +func traceKustomization(ctx context.Context, kubeClient client.Client, ksName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { + ks := &kustomizev1.Kustomization{} + ksReady := &metav1.Condition{} + err := kubeClient.Get(ctx, ksName, ks) + if err != nil { + return "", fmt.Errorf("failed to find kustomization: %w", err) + } + ksReady = meta.FindStatusCondition(ks.Status.Conditions, fluxmeta.ReadyCondition) + + var ksRepository *sourcev1.GitRepository + var ksRepositoryReady *metav1.Condition + if ks.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind { + ksRepository = &sourcev1.GitRepository{} + sourceNamespace := ks.Namespace + if ks.Spec.SourceRef.Namespace != "" { + sourceNamespace = ks.Spec.SourceRef.Namespace + } + err = kubeClient.Get(ctx, types.NamespacedName{ + Namespace: sourceNamespace, + Name: ks.Spec.SourceRef.Name, + }, ksRepository) + if err != nil { + return "", fmt.Errorf("failed to find GitRepository: %w", err) + } + ksRepositoryReady = meta.FindStatusCondition(ksRepository.Status.Conditions, fluxmeta.ReadyCondition) + } + + var traceTmpl = ` +Object: {{.ObjectName}} +{{- if .ObjectNamespace }} +Namespace: {{.ObjectNamespace}} +{{- end }} +Status: Managed by Flux +{{- if .Kustomization }} +--- +Kustomization: {{.Kustomization.Name}} +Namespace: {{.Kustomization.Namespace}} +{{- if .Kustomization.Spec.TargetNamespace }} +Target: {{.Kustomization.Spec.TargetNamespace}} +{{- end }} +Path: {{.Kustomization.Spec.Path}} +Revision: {{.Kustomization.Status.LastAppliedRevision}} +{{- if .KustomizationReady }} +Status: Last reconciled at {{.KustomizationReady.LastTransitionTime}} +Message: {{.KustomizationReady.Message}} +{{- else }} +Status: Unknown +{{- end }} +{{- end }} +{{- if .GitRepository }} +--- +GitRepository: {{.GitRepository.Name}} +Namespace: {{.GitRepository.Namespace}} +URL: {{.GitRepository.Spec.URL}} +{{- if .GitRepository.Spec.Reference.Branch }} +Branch: {{.GitRepository.Spec.Reference.Branch}} +{{- end }} +{{- if .GitRepository.Spec.Reference.Tag }} +Tag: {{.GitRepository.Spec.Reference.Tag}} +{{- else if .GitRepository.Spec.Reference.SemVer }} +Tag: {{.GitRepository.Spec.Reference.SemVer}} +{{- else if .GitRepository.Status.Artifact }} +Revision: {{.GitRepository.Status.Artifact.Revision}} +{{- end }} +{{- if .GitRepositoryReady }} +Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} +Message: {{.GitRepositoryReady.Message}} +{{- else }} +Status: Unknown +{{- end }} +{{- end }} +` + + traceResult := struct { + ObjectName string + ObjectNamespace string + Kustomization *kustomizev1.Kustomization + KustomizationReady *metav1.Condition + GitRepository *sourcev1.GitRepository + GitRepositoryReady *metav1.Condition + }{ + ObjectName: obj.GetKind() + "/" + obj.GetName(), + ObjectNamespace: obj.GetNamespace(), + Kustomization: ks, + KustomizationReady: ksReady, + GitRepository: ksRepository, + GitRepositoryReady: ksRepositoryReady, + } + + t, err := template.New("tmpl").Parse(traceTmpl) + if err != nil { + return "", err + } + + var data bytes.Buffer + writer := bufio.NewWriter(&data) + if err := t.Execute(writer, traceResult); err != nil { + return "", err + } + + if err := writer.Flush(); err != nil { + return "", err + } + + return data.String(), nil +} + +func traceHelm(ctx context.Context, kubeClient client.Client, hrName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { + hr := &helmv2.HelmRelease{} + hrReady := &metav1.Condition{} + err := kubeClient.Get(ctx, hrName, hr) + if err != nil { + return "", fmt.Errorf("failed to find HelmRelease: %w", err) + } + hrReady = meta.FindStatusCondition(hr.Status.Conditions, fluxmeta.ReadyCondition) + + var hrChart *sourcev1.HelmChart + var hrChartReady *metav1.Condition + if chart := hr.Status.HelmChart; chart != "" { + hrChart = &sourcev1.HelmChart{} + err = kubeClient.Get(ctx, utils.ParseNamespacedName(chart), hrChart) + if err != nil { + return "", fmt.Errorf("failed to find HelmChart: %w", err) + } + hrChartReady = meta.FindStatusCondition(hrChart.Status.Conditions, fluxmeta.ReadyCondition) + } + + var hrGitRepository *sourcev1.GitRepository + var hrGitRepositoryReady *metav1.Condition + if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind { + hrGitRepository = &sourcev1.GitRepository{} + sourceNamespace := hr.Namespace + if hr.Spec.Chart.Spec.SourceRef.Namespace != "" { + sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace + } + err = kubeClient.Get(ctx, types.NamespacedName{ + Namespace: sourceNamespace, + Name: hr.Spec.Chart.Spec.SourceRef.Name, + }, hrGitRepository) + if err != nil { + return "", fmt.Errorf("failed to find GitRepository: %w", err) + } + hrGitRepositoryReady = meta.FindStatusCondition(hrGitRepository.Status.Conditions, fluxmeta.ReadyCondition) + } + + var hrHelmRepository *sourcev1.HelmRepository + var hrHelmRepositoryReady *metav1.Condition + if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.HelmRepositoryKind { + hrHelmRepository = &sourcev1.HelmRepository{} + sourceNamespace := hr.Namespace + if hr.Spec.Chart.Spec.SourceRef.Namespace != "" { + sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace + } + err = kubeClient.Get(ctx, types.NamespacedName{ + Namespace: sourceNamespace, + Name: hr.Spec.Chart.Spec.SourceRef.Name, + }, hrHelmRepository) + if err != nil { + return "", fmt.Errorf("failed to find HelmRepository: %w", err) + } + hrHelmRepositoryReady = meta.FindStatusCondition(hrHelmRepository.Status.Conditions, fluxmeta.ReadyCondition) + } + + var traceTmpl = ` +Object: {{.ObjectName}} +{{- if .ObjectNamespace }} +Namespace: {{.ObjectNamespace}} +{{- end }} +Status: Managed by Flux +{{- if .HelmRelease }} +--- +HelmRelease: {{.HelmRelease.Name}} +Namespace: {{.HelmRelease.Namespace}} +{{- if .HelmRelease.Spec.TargetNamespace }} +Target: {{.HelmRelease.Spec.TargetNamespace}} +{{- end }} +Revision: {{.HelmRelease.Status.LastAppliedRevision}} +{{- if .HelmReleaseReady }} +Status: Last reconciled at {{.HelmReleaseReady.LastTransitionTime}} +Message: {{.HelmReleaseReady.Message}} +{{- else }} +Status: Unknown +{{- end }} +{{- end }} +{{- if .HelmChart }} +--- +HelmChart: {{.HelmChart.Name}} +Namespace: {{.HelmChart.Namespace}} +Chart: {{.HelmChart.Spec.Chart}} +Version: {{.HelmChart.Spec.Version}} +{{- if .HelmChart.Status.Artifact }} +Revision: {{.HelmChart.Status.Artifact.Revision}} +{{- end }} +{{- if .HelmChartReady }} +Status: Last reconciled at {{.HelmChartReady.LastTransitionTime}} +Message: {{.HelmChartReady.Message}} +{{- else }} +Status: Unknown +{{- end }} +{{- end }} +{{- if .HelmRepository }} +--- +HelmRepository: {{.HelmRepository.Name}} +Namespace: {{.HelmRepository.Namespace}} +URL: {{.HelmRepository.Spec.URL}} +{{- if .HelmRepository.Status.Artifact }} +Revision: {{.HelmRepository.Status.Artifact.Revision}} +{{- end }} +{{- if .HelmRepositoryReady }} +Status: Last reconciled at {{.HelmRepositoryReady.LastTransitionTime}} +Message: {{.HelmRepositoryReady.Message}} +{{- else }} +Status: Unknown +{{- end }} +{{- end }} +{{- if .GitRepository }} +--- +GitRepository: {{.GitRepository.Name}} +Namespace: {{.GitRepository.Namespace}} +URL: {{.GitRepository.Spec.URL}} +{{- if .GitRepository.Spec.Reference.Branch }} +Branch: {{.GitRepository.Spec.Reference.Branch}} +{{- end }} +{{- if .GitRepository.Spec.Reference.Tag }} +Tag: {{.GitRepository.Spec.Reference.Tag}} +{{- end }} +{{- if .GitRepository.Spec.Reference.Tag }} +Tag: {{.GitRepository.Spec.Reference.Tag}} +{{- end }} +{{- if .GitRepository.Spec.Reference.SemVer }} +Tag: {{.GitRepository.Spec.Reference.SemVer}} +{{- end }} +{{- if .GitRepository.Status.Artifact }} +Revision: {{.GitRepository.Status.Artifact.Revision}} +{{- end }} +{{- if .GitRepositoryReady }} +Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} +Message: {{.GitRepositoryReady.Message}} +{{- else }} +Status: Unknown +{{- end }} +{{- end }} +` + + traceResult := struct { + ObjectName string + ObjectNamespace string + HelmRelease *helmv2.HelmRelease + HelmReleaseReady *metav1.Condition + HelmChart *sourcev1.HelmChart + HelmChartReady *metav1.Condition + GitRepository *sourcev1.GitRepository + GitRepositoryReady *metav1.Condition + HelmRepository *sourcev1.HelmRepository + HelmRepositoryReady *metav1.Condition + }{ + ObjectName: obj.GetKind() + "/" + obj.GetName(), + ObjectNamespace: obj.GetNamespace(), + HelmRelease: hr, + HelmReleaseReady: hrReady, + HelmChart: hrChart, + HelmChartReady: hrChartReady, + GitRepository: hrGitRepository, + GitRepositoryReady: hrGitRepositoryReady, + HelmRepository: hrHelmRepository, + HelmRepositoryReady: hrHelmRepositoryReady, + } + + t, err := template.New("tmpl").Parse(traceTmpl) + if err != nil { + return "", err + } + + var data bytes.Buffer + writer := bufio.NewWriter(&data) + if err := t.Execute(writer, traceResult); err != nil { + return "", err + } + + if err := writer.Flush(); err != nil { + return "", err + } + + return data.String(), nil +} + +func isManagedByFlux(obj *unstructured.Unstructured, group string) (types.NamespacedName, bool) { + nameKey := fmt.Sprintf("%s/name", group) + namespaceKey := fmt.Sprintf("%s/namespace", group) + namespacedName := types.NamespacedName{} + + for k, v := range obj.GetLabels() { + if k == nameKey { + namespacedName.Name = v + } + if k == namespaceKey { + namespacedName.Namespace = v + } + } + + if namespacedName.Name == "" { + return namespacedName, false + } + return namespacedName, true +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index f606f4eb..958f861f 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -17,20 +17,12 @@ limitations under the License. package utils import ( - "bufio" "bytes" "context" "fmt" + "github.com/olekukonko/tablewriter" "io" "io/ioutil" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "text/template" - - "github.com/olekukonko/tablewriter" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -38,11 +30,17 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" sigyaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + "os" + "os/exec" + "path/filepath" + "runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" + "strings" helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" imageautov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" @@ -109,36 +107,6 @@ func ExecKubectlCommand(ctx context.Context, mode ExecMode, kubeConfigPath strin return "", nil } -func ExecTemplate(obj interface{}, tmpl, filename string) error { - t, err := template.New("tmpl").Parse(tmpl) - if err != nil { - return err - } - - var data bytes.Buffer - writer := bufio.NewWriter(&data) - if err := t.Execute(writer, obj); err != nil { - return err - } - - if err := writer.Flush(); err != nil { - return err - } - - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - - _, err = io.WriteString(file, data.String()) - if err != nil { - return err - } - - return file.Sync() -} - func KubeConfig(kubeConfigPath string, kubeContext string) (*rest.Config, error) { configFiles := SplitKubeConfigPath(kubeConfigPath) configOverrides := clientcmd.ConfigOverrides{} @@ -225,6 +193,21 @@ func ContainsEqualFoldItemString(s []string, e string) (string, bool) { return "", false } +// ParseNamespacedName extracts the NamespacedName of a resource +// based on the '/' format +func ParseNamespacedName(input string) types.NamespacedName { + parts := strings.Split(input, "/") + if len(parts) == 2 { + return types.NamespacedName{ + Namespace: parts[0], + Name: parts[1], + } + } + return types.NamespacedName{ + Name: input, + } +} + // ParseObjectKindName extracts the kind and name of a resource // based on the '/' format func ParseObjectKindName(input string) (kind, name string) { From 3f613341cba2b27421cf309730a931c70a88af3e Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 24 Jun 2021 09:56:42 +0300 Subject: [PATCH 2/3] Extend tracing to owner references Signed-off-by: Stefan Prodan --- cmd/flux/trace.go | 101 ++++++++++++++++++++++++++++++---------- internal/utils/utils.go | 13 +++--- 2 files changed, 84 insertions(+), 30 deletions(-) diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go index eee3da03..0c05ae28 100644 --- a/cmd/flux/trace.go +++ b/cmd/flux/trace.go @@ -44,7 +44,16 @@ var traceCmd = &cobra.Command{ 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.'`, Example: ` # Trace a Kubernetes Deployment - flux trace my-app --kind=deployment --api-version=apps/v1 --namespace=apps`, + flux trace my-app --kind=deployment --api-version=apps/v1 --namespace=apps + + # Trace a Kubernetes Pod + flux trace redis-master-0 --kind=pod --api-version=v1 -n redis + + # Trace a Kubernetes global object + flux trace redis --kind=namespace --api-version=v1 + + # Trace a Kubernetes custom resource + flux trace redis --kind=helmrelease --api-version=helm.toolkit.fluxcd.io/v2beta1 -n redis`, RunE: traceCmdRun, } @@ -107,7 +116,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to find object: %w", err) } - if ks, ok := isManagedByFlux(obj, kustomizev1.GroupVersion.Group); ok { + if ks, ok := isOwnerManagedByFlux(ctx, kubeClient, obj, kustomizev1.GroupVersion.Group); ok { report, err := traceKustomization(ctx, kubeClient, ks, obj) if err != nil { return err @@ -116,7 +125,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { return nil } - if hr, ok := isManagedByFlux(obj, helmv2.GroupVersion.Group); ok { + if hr, ok := isOwnerManagedByFlux(ctx, kubeClient, obj, helmv2.GroupVersion.Group); ok { report, err := traceHelm(ctx, kubeClient, hr, obj) if err != nil { return err @@ -182,18 +191,22 @@ Status: Unknown GitRepository: {{.GitRepository.Name}} Namespace: {{.GitRepository.Namespace}} URL: {{.GitRepository.Spec.URL}} -{{- if .GitRepository.Spec.Reference.Branch }} -Branch: {{.GitRepository.Spec.Reference.Branch}} -{{- end }} {{- if .GitRepository.Spec.Reference.Tag }} Tag: {{.GitRepository.Spec.Reference.Tag}} {{- else if .GitRepository.Spec.Reference.SemVer }} Tag: {{.GitRepository.Spec.Reference.SemVer}} -{{- else if .GitRepository.Status.Artifact }} +{{- else if .GitRepository.Spec.Reference.Branch }} +Branch: {{.GitRepository.Spec.Reference.Branch}} +{{- end }} +{{- if .GitRepository.Status.Artifact }} Revision: {{.GitRepository.Status.Artifact.Revision}} {{- end }} {{- if .GitRepositoryReady }} +{{- if eq .GitRepositoryReady.Status "False" }} +Status: Last reconciliation failed at {{.GitRepositoryReady.LastTransitionTime}} +{{- else }} Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} +{{- end }} Message: {{.GitRepositoryReady.Message}} {{- else }} Status: Unknown @@ -345,29 +358,28 @@ Status: Unknown {{- end }} {{- if .GitRepository }} --- -GitRepository: {{.GitRepository.Name}} -Namespace: {{.GitRepository.Namespace}} -URL: {{.GitRepository.Spec.URL}} -{{- if .GitRepository.Spec.Reference.Branch }} -Branch: {{.GitRepository.Spec.Reference.Branch}} -{{- end }} -{{- if .GitRepository.Spec.Reference.Tag }} -Tag: {{.GitRepository.Spec.Reference.Tag}} -{{- end }} +GitRepository: {{.GitRepository.Name}} +Namespace: {{.GitRepository.Namespace}} +URL: {{.GitRepository.Spec.URL}} {{- if .GitRepository.Spec.Reference.Tag }} -Tag: {{.GitRepository.Spec.Reference.Tag}} -{{- end }} -{{- if .GitRepository.Spec.Reference.SemVer }} -Tag: {{.GitRepository.Spec.Reference.SemVer}} +Tag: {{.GitRepository.Spec.Reference.Tag}} +{{- else if .GitRepository.Spec.Reference.SemVer }} +Tag: {{.GitRepository.Spec.Reference.SemVer}} +{{- else if .GitRepository.Spec.Reference.Branch }} +Branch: {{.GitRepository.Spec.Reference.Branch}} {{- end }} {{- if .GitRepository.Status.Artifact }} -Revision: {{.GitRepository.Status.Artifact.Revision}} +Revision: {{.GitRepository.Status.Artifact.Revision}} {{- end }} {{- if .GitRepositoryReady }} -Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} -Message: {{.GitRepositoryReady.Message}} +{{- if eq .GitRepositoryReady.Status "False" }} +Status: Last reconciliation failed at {{.GitRepositoryReady.LastTransitionTime}} {{- else }} -Status: Unknown +Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} +{{- end }} +Message: {{.GitRepositoryReady.Message}} +{{- else }} +Status: Unknown {{- end }} {{- end }} ` @@ -433,3 +445,44 @@ func isManagedByFlux(obj *unstructured.Unstructured, group string) (types.Namesp } return namespacedName, true } + +func isOwnerManagedByFlux(ctx context.Context, kubeClient client.Client, obj *unstructured.Unstructured, group string) (types.NamespacedName, bool) { + if n, ok := isManagedByFlux(obj, group); ok { + return n, true + } + + namespacedName := types.NamespacedName{} + for _, reference := range obj.GetOwnerReferences() { + owner := &unstructured.Unstructured{} + gv, err := schema.ParseGroupVersion(reference.APIVersion) + if err != nil { + return namespacedName, false + } + + owner.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: reference.Kind, + }) + + ownerName := types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: reference.Name, + } + + err = kubeClient.Get(ctx, ownerName, owner) + if err != nil { + return namespacedName, false + } + + if n, ok := isManagedByFlux(owner, group); ok { + return n, true + } + + if len(owner.GetOwnerReferences()) > 0 { + return isOwnerManagedByFlux(ctx, kubeClient, owner, group) + } + } + + return namespacedName, false +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 958f861f..f37249c1 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -20,9 +20,15 @@ import ( "bytes" "context" "fmt" - "github.com/olekukonko/tablewriter" "io" "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/olekukonko/tablewriter" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -34,13 +40,8 @@ import ( sigyaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - "os" - "os/exec" - "path/filepath" - "runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - "strings" helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" imageautov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" From 3f081ba2d9bfaf35215919220cfec1a1a13e0a5c Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 24 Jun 2021 13:46:22 +0300 Subject: [PATCH 3/3] Add flux trace to e2e tests Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 1c2e2b69..f5e9b2f2 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -91,6 +91,12 @@ jobs: --health-check="Deployment/frontend.dev" \ --health-check="Deployment/backend.dev" \ --health-check-timeout=3m + - name: flux trace + run: | + /tmp/flux trace frontend \ + --kind=deployment \ + --api-version=apps/v1 \ + --namespace=dev - name: flux reconcile kustomization --with-source run: | /tmp/flux reconcile kustomization podinfo --with-source