mirror of https://github.com/fluxcd/flux2.git
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 <stefan.prodan@gmail.com>pull/1555/head
parent
fab91d44c3
commit
4305b8a77d
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue