Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement flux debug kustomization command #5117

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions cmd/flux/debug_helmrelease.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ 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.`,
Long: withPreviewNote(`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),
RunE: debugHelmReleaseCmdRun,
Args: cobra.ExactArgs(1),
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
}

type debugHelmReleaseFlags struct {
name string
showStatus bool
showValues bool
}
Expand Down Expand Up @@ -86,6 +86,7 @@ func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
rootCmd.Println("# Status documentation: https://fluxcd.io/flux/components/helm/helmreleases/#helmrelease-status")
rootCmd.Print(string(status))
if debugHelmReleaseArgs.showValues {
rootCmd.Println("---")
Expand Down
16 changes: 16 additions & 0 deletions cmd/flux/debug_helmrelease_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
//go:build unit
// +build unit

/*
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 (
Expand Down
134 changes: 134 additions & 0 deletions cmd/flux/debug_kustomization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
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"
"errors"
"fmt"
"sort"
"strings"

kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/kustomize"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"

"github.com/fluxcd/flux2/v2/internal/utils"
)

var debugKustomizationCmd = &cobra.Command{
Use: "kustomization [name]",
Aliases: []string{"ks"},
Short: "Debug a Flux Kustomization resource",
Long: withPreviewNote(`The debug kustomization command can be used to troubleshoot failing Flux Kustomization reconciliations.
WARNING: This command will print sensitive information if Kubernetes Secrets are referenced in the Kustomization .spec.postBuild.substituteFrom field.`),
Example: ` # Print the status of a Flux Kustomization
flux debug ks podinfo --show-status

# Export the final variables used for post-build substitutions composed from referred ConfigMaps and Secrets
flux debug ks podinfo --show-vars > vars.env`,
RunE: debugKustomizationCmdRun,
Args: cobra.ExactArgs(1),
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
}

type debugKustomizationFlags struct {
showStatus bool
showVars bool
}

var debugKustomizationArgs debugKustomizationFlags

func init() {
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showStatus, "show-status", false, "print the status of the Flux Kustomization")
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showVars, "show-vars", false, "print the final vars of the Flux Kustomization in dot env format")
debugCmd.AddCommand(debugKustomizationCmd)
}

func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]

if (!debugKustomizationArgs.showStatus && !debugKustomizationArgs.showVars) ||
(debugKustomizationArgs.showStatus && debugKustomizationArgs.showVars) {
return fmt.Errorf("either --show-status or --show-vars must be set")
}

ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()

kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}

ks := &kustomizev1.Kustomization{}
ksName := types.NamespacedName{Namespace: *kubeconfigArgs.Namespace, Name: name}
if err := kubeClient.Get(ctx, ksName, ks); err != nil {
return err
}

if debugKustomizationArgs.showStatus {
status, err := yaml.Marshal(ks.Status)
if err != nil {
return err
}
rootCmd.Println("# Status documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#kustomization-status")
rootCmd.Print(string(status))
Copy link
Contributor

@darkowlzz darkowlzz Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I scaled KC to 0 and created a Kustomization to see the result.

$ flux -n default debug kustomization podinfo --show-status
observedGeneration: -1

I know what it means, but it feels like being a debug command for troubleshooting, we should be able to do some simple analysis and provide some human readable result to help the user understand what's going on, especially in cases like this.
In the usual case, the presence of conditions may help provide some sense of what could be wrong. But in this case, there's no hint of what it means.

If we go ahead and start providing some summary of the result, I think that would increase the scope of what can be done and make it more useful, but complex. Depending on the state, instead of trying to analyze, we can also provide a link to status docs of the respective resource, which the users can read and make sense of themselves. For example https://fluxcd.io/flux/components/kustomize/kustomizations/#kustomization-status.

For the above scenario, we may need to add a line in the docs about what -1 observed generation means.

Copy link
Member Author

@stefanprodan stefanprodan Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should show something like "Resource not initialised" for -1, when we'll implement the debug report.

For the --show-status output, I'm going to take your suggestion and add a link to the status doc on both commands, as a YAML comment.

return nil
}

if debugKustomizationArgs.showVars {
if ks.Spec.PostBuild == nil {
return errors.New("no post build substitutions found")
}

ksObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ks)
if err != nil {
return err
}

finalVars, err := kustomize.LoadVariables(ctx, kubeClient, unstructured.Unstructured{Object: ksObj})
if err != nil {
return err
}

if len(ks.Spec.PostBuild.Substitute) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostBuild is a pointer and the absence of any post build configuration results in a panic here.

Copy link
Member Author

@stefanprodan stefanprodan Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check at the top

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@darkowlzz great catch!

for k, v := range ks.Spec.PostBuild.Substitute {
// Remove new lines from the values as they are not supported.
// Replicates the controller behavior from
// https://github.com/fluxcd/pkg/blob/main/kustomize/kustomize_varsub.go
finalVars[k] = strings.ReplaceAll(v, "\n", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This string replacement of the value without any context is confusing. Maybe a link to https://github.com/fluxcd/pkg/blob/main/kustomize/kustomize_varsub.go or a helper function from there with more context would make it more readable to understand why this is being done here. The value transformation can also be tested in one central location and used everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added

}
}

keys := make([]string, 0, len(finalVars))
for k := range finalVars {
keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
rootCmd.Println(k + "=" + finalVars[k])
matheuscscp marked this conversation as resolved.
Show resolved Hide resolved
}
}

return nil
}
71 changes: 71 additions & 0 deletions cmd/flux/debug_kustomization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build unit
// +build unit

/*
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 (
"testing"
)

func TestDebugKustomization(t *testing.T) {
namespace := allocateNamespace("debug")

objectFile := "testdata/debug_kustomization/objects.yaml"
tmpl := map[string]string{
"fluxns": namespace,
}
testEnv.CreateObjectFile(objectFile, tmpl, t)

cases := []struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add 2 more tests: one for both flags set, and one for neither flags set?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure in LoadVariables(), maybe due to non-optional, non-existing substitute from object can also be a test case to make sure the command fails, instead of skipping such failures.

name string
arg string
goldenFile string
tmpl map[string]string
}{
{
"debug status",
"debug ks test --show-status --show-vars=false",
"testdata/debug_kustomization/status.golden.yaml",
tmpl,
},
{
"debug vars",
"debug ks test --show-vars --show-status=false",
"testdata/debug_kustomization/vars.golden.env",
tmpl,
},
{
"debug vars from",
"debug ks test-from --show-vars --show-status=false",
"testdata/debug_kustomization/vars-from.golden.env",
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)
})
}
}
16 changes: 16 additions & 0 deletions cmd/flux/export_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
//go:build unit
// +build unit

/*
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 (
Expand Down
1 change: 1 addition & 0 deletions cmd/flux/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ func resetCmdArgs() {
}
envsubstArgs = envsubstFlags{}
debugHelmReleaseArgs = debugHelmReleaseFlags{}
debugKustomizationArgs = debugKustomizationFlags{}
}

func isChangeError(err error) bool {
Expand Down
1 change: 1 addition & 0 deletions cmd/flux/testdata/debug_helmrelease/status.golden.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# Status documentation: https://fluxcd.io/flux/components/helm/helmreleases/#helmrelease-status
observedGeneration: -1
63 changes: 63 additions & 0 deletions cmd/flux/testdata/debug_kustomization/objects.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .fluxns }}
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: test
namespace: {{ .fluxns }}
spec:
sourceRef:
kind: GitRepository
name: test
interval: 1m
path: "./"
prune: true
postBuild:
substitute:
TEST_OVERRIDE: "in-line"
TEST_INLINE: "in-line"
substituteFrom:
- kind: ConfigMap
name: test
- kind: Secret
name: test
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: test-from
namespace: {{ .fluxns }}
spec:
sourceRef:
kind: GitRepository
name: test
interval: 1m
path: "./"
prune: true
postBuild:
substituteFrom:
- kind: ConfigMap
name: test
- kind: Secret
name: test
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test
namespace: {{ .fluxns }}
data:
TEST_OVERRIDE: "cm"
TEST_CM: "cm"
---
apiVersion: v1
kind: Secret
metadata:
name: test
namespace: {{ .fluxns }}
stringData:
TEST_OVERRIDE: "secret"
TEST_SECRET: "secret"
2 changes: 2 additions & 0 deletions cmd/flux/testdata/debug_kustomization/status.golden.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Status documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#kustomization-status
observedGeneration: -1
3 changes: 3 additions & 0 deletions cmd/flux/testdata/debug_kustomization/vars-from.golden.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TEST_CM=cm
TEST_OVERRIDE=secret
TEST_SECRET=secret
4 changes: 4 additions & 0 deletions cmd/flux/testdata/debug_kustomization/vars.golden.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TEST_CM=cm
TEST_INLINE=in-line
TEST_OVERRIDE=in-line
TEST_SECRET=secret
16 changes: 16 additions & 0 deletions cmd/flux/trace_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
//go:build unit
// +build unit

/*
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 (
Expand Down
Loading