diff --git a/cmd/flux/debug.go b/cmd/flux/debug.go new file mode 100644 index 0000000000..355150cb6e --- /dev/null +++ b/cmd/flux/debug.go @@ -0,0 +1,31 @@ +/* +Copyright 2024 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 ( + "github.com/spf13/cobra" +) + +var debugCmd = &cobra.Command{ + Use: "debug", + Short: "Debug a flux resource", + Long: `The debug command can be used to troubleshoot failing resource reconciliations.`, +} + +func init() { + rootCmd.AddCommand(debugCmd) +} diff --git a/cmd/flux/debug_helmrelease.go b/cmd/flux/debug_helmrelease.go new file mode 100644 index 0000000000..db95639596 --- /dev/null +++ b/cmd/flux/debug_helmrelease.go @@ -0,0 +1,122 @@ +/* +Copyright 2024 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 ( + "context" + "fmt" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/chartutil" + "github.com/go-logr/logr" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/yaml" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var debugHelmReleaseCmd = &cobra.Command{ + Use: "helmrelease [name]", + Aliases: []string{"hr"}, + Short: "Debug a HelmRelease resource", + Long: `The debug helmrelease command can be used to troubleshoot failing Helm release reconciliations. +WARNING: This command will print sensitive information if Kubernetes Secrets are referenced in the HelmRelease .spec.valuesFrom field.`, + Example: ` # Print the status of a Helm release + flux debug hr podinfo --show-status + + # Export the final values of a Helm release composed from referred ConfigMaps and Secrets + flux debug hr podinfo --show-values > values.yaml`, + RunE: debugHelmReleaseCmdRun, + Args: cobra.ExactArgs(1), +} + +type debugHelmReleaseFlags struct { + name string + showStatus bool + showValues bool +} + +var debugHelmReleaseArgs debugHelmReleaseFlags + +func init() { + debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showStatus, "show-status", false, "print the status of the Helm release") + debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showValues, "show-values", false, "print the final values of the Helm release") + debugCmd.AddCommand(debugHelmReleaseCmd) +} + +func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { + name := args[0] + + if debugHelmReleaseArgs.showStatus == false && debugHelmReleaseArgs.showValues == false { + return fmt.Errorf("either --show-status or --show-values must be set") + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + hr := &helmv2.HelmRelease{} + hrName := types.NamespacedName{Namespace: *kubeconfigArgs.Namespace, Name: name} + if err := kubeClient.Get(ctx, hrName, hr); err != nil { + return err + } + + if debugHelmReleaseArgs.showStatus { + status, err := yaml.Marshal(hr.Status) + if err != nil { + return err + } + rootCmd.Print(string(status)) + } + + if debugHelmReleaseArgs.showValues { + // TODO(stefan): remove the mapping when helm-controller/api v1.2.0 has been released + var valuesRefs []meta.ValuesReference + for _, source := range hr.Spec.ValuesFrom { + valuesRefs = append(valuesRefs, meta.ValuesReference{ + Kind: source.Kind, + Name: source.Name, + ValuesKey: source.ValuesKey, + Optional: source.Optional, + }) + } + + finalValues, err := chartutil.ChartValuesFromReferences(ctx, + logr.Discard(), + kubeClient, + hr.GetNamespace(), + hr.GetValues(), + valuesRefs...) + if err != nil { + return err + } + + values, err := yaml.Marshal(finalValues) + if err != nil { + return err + } + rootCmd.Print(string(values)) + } + + return nil +} diff --git a/cmd/flux/debug_helmrelease_test.go b/cmd/flux/debug_helmrelease_test.go new file mode 100644 index 0000000000..cb65df41bd --- /dev/null +++ b/cmd/flux/debug_helmrelease_test.go @@ -0,0 +1,55 @@ +//go:build unit +// +build unit + +package main + +import ( + "testing" +) + +func TestDebugHelmRelease(t *testing.T) { + namespace := allocateNamespace("debug") + + objectFile := "testdata/debug_helmrelease/objects.yaml" + tmpl := map[string]string{ + "fluxns": namespace, + } + testEnv.CreateObjectFile(objectFile, tmpl, t) + + cases := []struct { + name string + arg string + goldenFile string + tmpl map[string]string + }{ + { + "debug status", + "debug helmrelease test-values-inline --show-status --show-values=false", + "testdata/debug_helmrelease/status.golden.yaml", + tmpl, + }, + { + "debug values", + "debug helmrelease test-values-inline --show-values --show-status=false", + "testdata/debug_helmrelease/values-inline.golden.yaml", + tmpl, + }, + { + "debug values from", + "debug helmrelease test-values-from --show-values --show-status=false", + "testdata/debug_helmrelease/values-from.golden.yaml", + tmpl, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.arg + " -n=" + namespace, + assert: assertGoldenTemplateFile(tt.goldenFile, tmpl), + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 8d09f93882..e814c5854e 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -469,6 +469,7 @@ func resetCmdArgs() { output: "yaml", } envsubstArgs = envsubstFlags{} + debugHelmReleaseArgs = debugHelmReleaseFlags{} } func isChangeError(err error) bool { diff --git a/cmd/flux/testdata/debug_helmrelease/objects.yaml b/cmd/flux/testdata/debug_helmrelease/objects.yaml new file mode 100644 index 0000000000..50a96f1a47 --- /dev/null +++ b/cmd/flux/testdata/debug_helmrelease/objects.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: test-values-inline + namespace: {{ .fluxns }} +spec: + chartRef: + kind: OCIRepository + name: podinfo + interval: 5m0s + values: + image: + repository: stefanprodan/podinfo + tag: 5.0.0 +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: test-values-from + namespace: {{ .fluxns }} +spec: + chartRef: + kind: OCIRepository + name: podinfo + interval: 5m0s + values: + image: + repository: stefanprodan/podinfo + tag: 5.0.0 + valuesFrom: + - kind: ConfigMap + name: test + - kind: Secret + name: test + valuesKey: secrets.yaml + - kind: ConfigMap + name: none + optional: true +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test + namespace: {{ .fluxns }} +data: + values.yaml: | + cm: "test" + override: "cm" +--- +apiVersion: v1 +kind: Secret +metadata: + name: test + namespace: {{ .fluxns }} +stringData: + secrets.yaml: | + secret: "test" + override: "secret" diff --git a/cmd/flux/testdata/debug_helmrelease/status.golden.yaml b/cmd/flux/testdata/debug_helmrelease/status.golden.yaml new file mode 100644 index 0000000000..8f988e76af --- /dev/null +++ b/cmd/flux/testdata/debug_helmrelease/status.golden.yaml @@ -0,0 +1 @@ +observedGeneration: -1 diff --git a/cmd/flux/testdata/debug_helmrelease/values-from.golden.yaml b/cmd/flux/testdata/debug_helmrelease/values-from.golden.yaml new file mode 100644 index 0000000000..c625f83159 --- /dev/null +++ b/cmd/flux/testdata/debug_helmrelease/values-from.golden.yaml @@ -0,0 +1,6 @@ +cm: test +image: + repository: stefanprodan/podinfo + tag: 5.0.0 +override: secret +secret: test diff --git a/cmd/flux/testdata/debug_helmrelease/values-inline.golden.yaml b/cmd/flux/testdata/debug_helmrelease/values-inline.golden.yaml new file mode 100644 index 0000000000..81afc91b48 --- /dev/null +++ b/cmd/flux/testdata/debug_helmrelease/values-inline.golden.yaml @@ -0,0 +1,3 @@ +image: + repository: stefanprodan/podinfo + tag: 5.0.0 diff --git a/go.mod b/go.mod index 558c146995..547fb81e5e 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/fluxcd/notification-controller/api v1.4.0 github.com/fluxcd/pkg/apis/event v0.11.0 github.com/fluxcd/pkg/apis/meta v1.8.0 + github.com/fluxcd/pkg/chartutil v1.0.0 github.com/fluxcd/pkg/envsubst v1.2.0 github.com/fluxcd/pkg/git v0.22.0 github.com/fluxcd/pkg/git/gogit v0.22.0 @@ -172,10 +173,12 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/spdystream v0.4.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -210,6 +213,9 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/go-gitlab v0.114.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.54.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.54.0 // indirect @@ -248,6 +254,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + helm.sh/helm/v3 v3.16.3 // indirect k8s.io/component-base v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect diff --git a/go.sum b/go.sum index 5ae7f61ede..5938b4acbf 100644 --- a/go.sum +++ b/go.sum @@ -141,8 +141,8 @@ github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapw github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= -github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -177,6 +177,8 @@ github.com/fluxcd/pkg/apis/meta v1.8.0 h1:wF7MJ3mu5ds9Y/exWU1yU0YyDb8s1VwwQnZYuM github.com/fluxcd/pkg/apis/meta v1.8.0/go.mod h1:OJGH7I//SNO6zcso80oBRuf5H8oU8etZDeTgCcH7qHo= github.com/fluxcd/pkg/auth v0.1.0 h1:qODzb3BeW/8hkzVeq+8GqZlOEm39xeaxAkJn02Jc+Fo= github.com/fluxcd/pkg/auth v0.1.0/go.mod h1:34t6toih5y9W53kIBTubQsGwKsNnBlc7VDE+FqZenyk= +github.com/fluxcd/pkg/chartutil v1.0.0 h1:Hj5mPiUp/nanZPVK7Ur0TDN4BCMhuoxKjvAmBbnX7DE= +github.com/fluxcd/pkg/chartutil v1.0.0/go.mod h1:GBo3G78aiK48BppJ/YoDUv8L1NDLHrMpK3K5uiazQ0A= github.com/fluxcd/pkg/envsubst v1.2.0 h1:hbn3Zie/+/88uDTw4EbqD9/SXO+LP97FZhZFzPtVVug= github.com/fluxcd/pkg/envsubst v1.2.0/go.mod h1:ws8rM2Zmy+wm4iryW6pnTBuerMtwT5JQpDty+sHtCG8= github.com/fluxcd/pkg/git v0.22.0 h1:3O7XgQEaCgjC0irKkXkT1wWi5r1o4AnrxGhV8Mao85o= @@ -401,6 +403,8 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -409,6 +413,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -538,6 +544,13 @@ github.com/xanzy/go-gitlab v0.114.0 h1:0wQr/KBckwrZPfEMjRqpUz0HmsKKON9UhCYv9KDy1 github.com/xanzy/go-gitlab v0.114.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -727,6 +740,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +helm.sh/helm/v3 v3.16.3 h1:kb8bSxMeRJ+knsK/ovvlaVPfdis0X3/ZhYCSFRP+YmY= +helm.sh/helm/v3 v3.16.3/go.mod h1:zeVWGDR4JJgiRbT3AnNsjYaX8OTJlIE9zC+Q7F7iUSU= k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= k8s.io/apiextensions-apiserver v0.31.3 h1:+GFGj2qFiU7rGCsA5o+p/rul1OQIq6oYpQw4+u+nciE=