-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
feat: edit set secret
* Add new functionality to allow editing a secret in a kustomization file. Initial implementation supports only --from-literal option. * Refactor edit set configmap to reuse some testing bits.
1 parent
a0a9bdf
commit 5f15ceb
Showing
5 changed files
with
550 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
// Copyright 2023 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package set | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/spf13/cobra" | ||
"golang.org/x/exp/slices" | ||
"sigs.k8s.io/kustomize/api/ifc" | ||
"sigs.k8s.io/kustomize/api/konfig" | ||
"sigs.k8s.io/kustomize/api/resource" | ||
"sigs.k8s.io/kustomize/api/types" | ||
"sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile" | ||
"sigs.k8s.io/kustomize/kustomize/v5/commands/internal/util" | ||
"sigs.k8s.io/kustomize/kyaml/filesys" | ||
) | ||
|
||
func newCmdSetSecret( | ||
fSys filesys.FileSystem, | ||
ldr ifc.KvLoader, | ||
rf *resource.Factory, | ||
) *cobra.Command { | ||
var flags util.ConfigMapSecretFlagsAndArgs | ||
cmd := &cobra.Command{ | ||
Use: "secret NAME [--from-literal=key1=value1] [--namespace=namespace-name] [--new-namespace=new-namespace-name]", | ||
Short: fmt.Sprintf("Edits the value for an existing key for a secret in the %s file", konfig.DefaultKustomizationFileName()), | ||
Long: fmt.Sprintf(`Edits the value for an existing key in an existing secret in the %s file. | ||
Both secret name and key name must exist for this command to succeed.`, konfig.DefaultKustomizationFileName()), | ||
Example: fmt.Sprintf(` | ||
# Edits an existing secret in the %[1]s file, changing the value of key1 to 2 | ||
kustomize edit set secret my-secret --from-literal=key1=2 | ||
# Edits an existing secret in the %[1]s file, changing namespace to 'new-namespace' | ||
kustomize edit set secret my-secret --namespace=current-namespace --new-namespace=new-namespace | ||
`, konfig.DefaultKustomizationFileName()), | ||
RunE: func(_ *cobra.Command, args []string) error { | ||
return runEditSetSecret(flags, fSys, args, ldr, rf) | ||
}, | ||
} | ||
|
||
cmd.Flags().StringArrayVar( | ||
&flags.LiteralSources, | ||
util.FromLiteralFlag, | ||
[]string{}, | ||
"Specify an existing key and a new value to update a Secret (i.e. mykey=newvalue)") | ||
cmd.Flags().StringVar( | ||
&flags.Namespace, | ||
util.NamespaceFlag, | ||
"", | ||
"Current namespace of the target Secret") | ||
cmd.Flags().StringVar( | ||
&flags.NewNamespace, | ||
util.NewNamespaceFlag, | ||
"", | ||
"New namespace value for the target Secret") | ||
|
||
return cmd | ||
} | ||
|
||
func runEditSetSecret( | ||
flags util.ConfigMapSecretFlagsAndArgs, | ||
fSys filesys.FileSystem, | ||
args []string, | ||
ldr ifc.KvLoader, | ||
rf *resource.Factory, | ||
) error { | ||
err := flags.ExpandFileSource(fSys) | ||
if err != nil { | ||
return fmt.Errorf("failed to expand file source: %w", err) | ||
} | ||
|
||
err = flags.ValidateSet(args) | ||
if err != nil { | ||
return fmt.Errorf("failed to validate flags: %w", err) | ||
} | ||
|
||
// Load the kustomization file. | ||
mf, err := kustfile.NewKustomizationFile(fSys) | ||
if err != nil { | ||
return fmt.Errorf("failed to load kustomization file: %w", err) | ||
} | ||
|
||
kustomization, err := mf.Read() | ||
if err != nil { | ||
return fmt.Errorf("failed to read kustomization file: %w", err) | ||
} | ||
|
||
// Updates the existing Secret | ||
err = setSecret(ldr, kustomization, flags, rf) | ||
if err != nil { | ||
return fmt.Errorf("failed to create secret: %w", err) | ||
} | ||
|
||
// Write out the kustomization file with added secret. | ||
err = mf.Write(kustomization) | ||
if err != nil { | ||
return fmt.Errorf("failed to write kustomization file: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func setSecret( | ||
ldr ifc.KvLoader, | ||
k *types.Kustomization, | ||
flags util.ConfigMapSecretFlagsAndArgs, | ||
rf *resource.Factory, | ||
) error { | ||
args, err := findSecretArgs(k, flags.Name, flags.Namespace) | ||
if err != nil { | ||
return fmt.Errorf("could not set new Secret value: %w", err) | ||
} | ||
|
||
if len(flags.LiteralSources) > 0 { | ||
err := util.UpdateLiteralSources(&args.GeneratorArgs, flags) | ||
if err != nil { | ||
return fmt.Errorf("failed to update literal sources: %w", err) | ||
} | ||
} | ||
|
||
// update namespace to new one | ||
if flags.NewNamespace != "" { | ||
args.Namespace = flags.NewNamespace | ||
} | ||
|
||
// Validate by trying to create corev1.secret. | ||
args.Options = types.MergeGlobalOptionsIntoLocal( | ||
args.Options, k.GeneratorOptions) | ||
|
||
_, err = rf.MakeSecret(ldr, args) | ||
if err != nil { | ||
return fmt.Errorf("failed to validate Secret structure: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// findSecretArgs finds the generator arguments corresponding to the specified | ||
// Secret name. Secret must exist for this command to be successful. | ||
func findSecretArgs(m *types.Kustomization, name, namespace string) (*types.SecretArgs, error) { | ||
cmIndex := slices.IndexFunc(m.SecretGenerator, func(cmArgs types.SecretArgs) bool { | ||
return name == cmArgs.Name && util.NamespaceEqual(namespace, cmArgs.Namespace) | ||
}) | ||
|
||
if cmIndex == -1 { | ||
return nil, fmt.Errorf("unable to find Secret with name '%q'", name) | ||
} | ||
|
||
return &m.SecretGenerator[cmIndex], nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
// Copyright 2023 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
// The duplicate linter is reporting that setconfigmap_test.go and this file are duplicates, which is not true. | ||
// Disabling lint for these two files specifically to work around that. | ||
// | ||
//nolint:dupl | ||
package set | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
testutils_test "sigs.k8s.io/kustomize/kustomize/v5/commands/internal/configmapsecret" | ||
) | ||
|
||
func TestFailureCasesEditSetSecret(t *testing.T) { | ||
testCases := []testutils_test.FailureCase{ | ||
{ | ||
Name: "fails to set a value because key doesn't exist", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- key1=val1 | ||
name: test-secret | ||
type: Opaque | ||
- literals: | ||
- key3=val1 | ||
name: test-secret-2 | ||
namespace: test-ns | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--from-literal=key3=val2"}, | ||
ExpectedErrorMsg: "key 'key3' not found in resource", | ||
}, | ||
{ | ||
Name: "fails to set a value because secret doesn't exist", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- key1=val1 | ||
name: test-secret | ||
type: Opaque | ||
- literals: | ||
- key2=value | ||
name: another-secret | ||
namespace: a-namespace | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret2", "--from-literal=key3=val2"}, | ||
ExpectedErrorMsg: "unable to find Secret with name '\"test-secret2\"'", | ||
}, | ||
{ | ||
Name: "fails validation because no attributes are being changed", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- a-test-key=a-test-value | ||
name: test-secret | ||
namespace: test-ns | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--namespace=test-ns"}, | ||
ExpectedErrorMsg: "at least one of [--from-literal, --new-namespace] must be specified", | ||
}, | ||
{ | ||
Name: "fails when a literal source doesn't have a key", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- some-key=some-value | ||
name: test-secret | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--from-literal=value"}, | ||
ExpectedErrorMsg: "literal values must be specified in the key=value format", | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
kustomization, err := testutils_test.SetupEditSetConfigMapSecretTest(t, newCmdSetSecret, tc.KustomizationFileContent, tc.Args) | ||
|
||
require.Nil(t, kustomization) | ||
require.Error(t, err) | ||
require.Contains(t, err.Error(), tc.ExpectedErrorMsg) | ||
}) | ||
} | ||
} | ||
|
||
func TestSuccessCasesEditSetSecret(t *testing.T) { | ||
testCases := []testutils_test.SuccessCase{ | ||
{ | ||
Name: "set a value successfully", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- random-key=random-value | ||
- key1=value | ||
name: test-secret | ||
type: Opaque | ||
`, | ||
ExpectedLiterals: []string{"key1=val2", "random-key=random-value"}, | ||
Args: []string{"test-secret", "--from-literal=key1=val2"}, | ||
}, | ||
{ | ||
Name: "successfully update namespace of target secret", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- a-key=test | ||
- another-key=value | ||
name: test-secret | ||
namespace: test-ns | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--namespace=test-ns", "--new-namespace=test-new-ns"}, | ||
ExpectedNamespace: "test-new-ns", | ||
}, | ||
{ | ||
Name: "successfully update namespace of target secret with empty namespace in file and namespace specified in command", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- key1=value1 | ||
- another-key=another-value | ||
name: test-secret | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--namespace=default", "--new-namespace=test-new-ns"}, | ||
ExpectedNamespace: "test-new-ns", | ||
}, | ||
{ | ||
Name: "successfully update namespace of target secret with default namespace and no namespace specified in command", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- random-key=random-value | ||
name: test-secret | ||
namespace: default | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--new-namespace=test-new-ns"}, | ||
ExpectedNamespace: "test-new-ns", | ||
}, | ||
{ | ||
Name: "successfully update literal source of target secret with empty namespace in file and namespace specified in command", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- a-key=a-value | ||
- key1=value | ||
name: test-secret | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--namespace=default", "--from-literal=key1=val2"}, | ||
ExpectedLiterals: []string{"a-key=a-value", "key1=val2"}, | ||
}, | ||
{ | ||
Name: "successfully update namespace of target secret with default namespace and no namespace specified in command", | ||
KustomizationFileContent: ` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
secretGenerator: | ||
- literals: | ||
- test-secret-key=value | ||
- key1=val1 | ||
name: test-secret | ||
namespace: default | ||
type: Opaque | ||
`, | ||
Args: []string{"test-secret", "--from-literal=key1=val2"}, | ||
ExpectedLiterals: []string{"test-secret-key=value", "key1=val2"}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
kustomization, err := testutils_test.SetupEditSetConfigMapSecretTest(t, newCmdSetSecret, tc.KustomizationFileContent, tc.Args) | ||
|
||
require.NoError(t, err) | ||
require.NotNil(t, kustomization) | ||
require.NotEmpty(t, kustomization.SecretGenerator) | ||
require.Greater(t, len(kustomization.SecretGenerator), 0) | ||
|
||
if tc.ExpectedNamespace != "" { | ||
require.Equal(t, tc.ExpectedNamespace, kustomization.SecretGenerator[0].Namespace) | ||
} | ||
|
||
if len(tc.ExpectedLiterals) > 0 { | ||
require.ElementsMatch(t, tc.ExpectedLiterals, kustomization.SecretGenerator[0].LiteralSources) | ||
} | ||
}) | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
kustomize/commands/internal/configmapsecret/configmapsecret_testutils.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// Copyright 2023 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package configmapsecret_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/stretchr/testify/require" | ||
"sigs.k8s.io/kustomize/api/ifc" | ||
"sigs.k8s.io/kustomize/api/kv" | ||
"sigs.k8s.io/kustomize/api/pkg/loader" | ||
"sigs.k8s.io/kustomize/api/provider" | ||
"sigs.k8s.io/kustomize/api/resource" | ||
"sigs.k8s.io/kustomize/api/types" | ||
"sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile" | ||
testutils_test "sigs.k8s.io/kustomize/kustomize/v5/commands/internal/testutils" | ||
"sigs.k8s.io/kustomize/kyaml/filesys" | ||
) | ||
|
||
// FailureCase specifies a test case for a failure in the 'edit set configmap'/'edit set secret' commands. | ||
type FailureCase struct { | ||
// The name of the test case | ||
Name string | ||
|
||
// The kustomization file content for the test case in YAML format | ||
KustomizationFileContent string | ||
|
||
// Arguments passed to the test case command | ||
Args []string | ||
|
||
// The expected error message for the test case | ||
ExpectedErrorMsg string | ||
} | ||
|
||
// SuccessCase specifies a test case for a success in the 'edit set configmap'/'edit set secret' commands. | ||
type SuccessCase struct { | ||
// The name of the test case | ||
Name string | ||
|
||
// The kustomization file content for the test case in YAML format | ||
KustomizationFileContent string | ||
|
||
// Arguments passed to the test case command | ||
Args []string | ||
|
||
// List of expected literals for the result of the test case | ||
ExpectedLiterals []string | ||
|
||
// The expected namespace for the result of the test case | ||
ExpectedNamespace string | ||
} | ||
|
||
func SetupEditSetConfigMapSecretTest( | ||
t *testing.T, | ||
command func(filesys.FileSystem, ifc.KvLoader, *resource.Factory) *cobra.Command, | ||
input string, | ||
args []string, | ||
) (*types.Kustomization, error) { | ||
t.Helper() | ||
fSys := filesys.MakeFsInMemory() | ||
pvd := provider.NewDefaultDepProvider() | ||
|
||
cmd := command( | ||
fSys, | ||
kv.NewLoader( | ||
loader.NewFileLoaderAtCwd(fSys), | ||
pvd.GetFieldValidator()), | ||
pvd.GetResourceFactory(), | ||
) | ||
|
||
testutils_test.WriteTestKustomizationWith(fSys, []byte(input)) | ||
|
||
cmd.SetArgs(args) | ||
err := cmd.Execute() | ||
|
||
//nolint: wrapcheck | ||
// this needs to be bubbled up for checking in the test | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
require.NoError(t, err) | ||
|
||
_, err = testutils_test.ReadTestKustomization(fSys) | ||
require.NoError(t, err) | ||
|
||
mf, err := kustfile.NewKustomizationFile(fSys) | ||
require.NoError(t, err) | ||
|
||
kustomization, err := mf.Read() | ||
require.NoError(t, err) | ||
|
||
return kustomization, nil | ||
} |