mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
Introduce CLI for ApplySet-based pruning (#115979)
* Introduce CLI for ApplySet-based pruning * Address feedback from justinsb * Fix parent namespace sourcing and restrict types. Increase test coverage.
This commit is contained in:
parent
911631a6e0
commit
0dd346673c
@ -64,6 +64,7 @@ type ApplyFlags struct {
|
||||
Selector string
|
||||
Prune bool
|
||||
PruneResources []prune.Resource
|
||||
ApplySetRef string
|
||||
All bool
|
||||
Overwrite bool
|
||||
OpenAPIPatch bool
|
||||
@ -130,6 +131,10 @@ type ApplyOptions struct {
|
||||
// Function run after all objects have been applied.
|
||||
// The standard PostProcessorFn is "PrintAndPrunePostProcessor()".
|
||||
PostProcessorFn func() error
|
||||
|
||||
// ApplySet tracks the set of objects that have been applied, for the purposes of pruning.
|
||||
// See git.k8s.io/enhancements/keps/sig-cli/3659-kubectl-apply-prune
|
||||
ApplySet *ApplySet
|
||||
}
|
||||
|
||||
var (
|
||||
@ -223,13 +228,9 @@ func (flags *ApplyFlags) AddFlags(cmd *cobra.Command) {
|
||||
cmdutil.AddServerSideApplyFlags(cmd)
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, FieldManagerClientSideApply)
|
||||
cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector)
|
||||
cmdutil.AddPruningFlags(cmd, &flags.Prune, &flags.PruneAllowlist, &flags.PruneWhitelist, &flags.All, &flags.ApplySetRef)
|
||||
|
||||
cmd.Flags().BoolVar(&flags.Overwrite, "overwrite", flags.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration")
|
||||
cmd.Flags().BoolVar(&flags.Prune, "prune", flags.Prune, "Automatically delete resource objects, that do not appear in the configs and are created by either apply or create --save-config. Should be used with either -l or --all.")
|
||||
cmd.Flags().BoolVar(&flags.All, "all", flags.All, "Select all resources in the namespace of the specified resource types.")
|
||||
cmd.Flags().StringArrayVar(&flags.PruneAllowlist, "prune-allowlist", flags.PruneAllowlist, "Overwrite the default allowlist with <group/version/kind> for --prune")
|
||||
cmd.Flags().StringArrayVar(&flags.PruneWhitelist, "prune-whitelist", flags.PruneWhitelist, "Overwrite the default whitelist with <group/version/kind> for --prune") // TODO: Remove this in kubectl 1.28 or later
|
||||
cmd.Flags().MarkDeprecated("prune-whitelist", "Use --prune-allowlist instead.")
|
||||
cmd.Flags().BoolVar(&flags.OpenAPIPatch, "openapi-patch", flags.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")
|
||||
}
|
||||
|
||||
@ -297,6 +298,17 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var applySet *ApplySet
|
||||
if flags.ApplySetRef != "" {
|
||||
var applySetNs string
|
||||
// ApplySet uses the namespace value from the flag, but not from the kubeconfig or defaults
|
||||
if enforceNamespace {
|
||||
applySetNs = namespace
|
||||
}
|
||||
if applySet, err = NewApplySet(flags.ApplySetRef, applySetNs, mapper); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if flags.Prune {
|
||||
pruneAllowlist := slice.ToSet(flags.PruneAllowlist, flags.PruneWhitelist)
|
||||
flags.PruneResources, err = prune.ParseResources(mapper, pruneAllowlist)
|
||||
@ -342,6 +354,8 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
|
||||
|
||||
VisitedUids: sets.NewString(),
|
||||
VisitedNamespaces: sets.NewString(),
|
||||
|
||||
ApplySet: applySet,
|
||||
}
|
||||
|
||||
o.PostProcessorFn = o.PrintAndPrunePostProcessor()
|
||||
@ -371,19 +385,40 @@ func (o *ApplyOptions) Validate() error {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
|
||||
if o.Prune && !o.All && o.Selector == "" {
|
||||
return fmt.Errorf("all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector")
|
||||
if o.ApplySet != nil {
|
||||
if !o.Prune {
|
||||
return fmt.Errorf("--applyset requires --prune")
|
||||
}
|
||||
if err := o.ApplySet.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if o.Prune {
|
||||
// Do not force the recreation of an object(s) if we're pruning; this can cause
|
||||
// undefined behavior since object UID's change.
|
||||
if o.DeleteOptions.ForceDeletion {
|
||||
return fmt.Errorf("--force cannot be used with --prune")
|
||||
}
|
||||
|
||||
// Do not force the recreation of an object(s) if we're pruning; this can cause
|
||||
// undefined behavior since object UID's change.
|
||||
if o.Prune && o.DeleteOptions.ForceDeletion {
|
||||
return fmt.Errorf("--force cannot be used with --prune")
|
||||
}
|
||||
|
||||
// Currently do not support pruning objects which are server-side applied.
|
||||
if o.Prune && o.ServerSideApply {
|
||||
return fmt.Errorf("--prune is in alpha and doesn't currently work on objects created by server-side apply")
|
||||
if o.ApplySet != nil {
|
||||
if o.All {
|
||||
return fmt.Errorf("--all is incompatible with --applyset")
|
||||
} else if o.Selector != "" {
|
||||
return fmt.Errorf("--selector is incompatible with --applyset")
|
||||
} else if len(o.PruneResources) > 0 {
|
||||
return fmt.Errorf("--prune-allowlist is incompatible with --applyset")
|
||||
} else {
|
||||
// TODO: remove this once ApplySet implementation is complete
|
||||
return fmt.Errorf("--applyset is not yet supported")
|
||||
}
|
||||
} else {
|
||||
if !o.All && o.Selector == "" {
|
||||
return fmt.Errorf("all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector")
|
||||
}
|
||||
if o.ServerSideApply {
|
||||
return fmt.Errorf("--prune is in alpha and doesn't currently work on objects created by server-side apply")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -955,8 +990,13 @@ func (o *ApplyOptions) PrintAndPrunePostProcessor() func() error {
|
||||
}
|
||||
|
||||
if o.Prune {
|
||||
p := newPruner(o)
|
||||
return p.pruneAll(o)
|
||||
if cmdutil.ApplySet.IsEnabled() && o.ApplySet != nil {
|
||||
p := newApplySetPruner(o)
|
||||
return p.pruneAll()
|
||||
} else {
|
||||
p := newPruner(o)
|
||||
return p.pruneAll(o)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -29,7 +29,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@ -84,26 +87,40 @@ func TestApplyExtraArgsFail(t *testing.T) {
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
c := NewCmdApply("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
||||
if validateApplyArgs(c, []string{"rc"}) == nil {
|
||||
t.Fatalf("unexpected non-error")
|
||||
}
|
||||
cmd := &cobra.Command{}
|
||||
flags := NewApplyFlags(genericclioptions.NewTestIOStreamsDiscard())
|
||||
flags.AddFlags(cmd)
|
||||
_, err := flags.ToOptions(f, cmd, "kubectl", []string{"rc"})
|
||||
require.EqualError(t, err, "Unexpected args: [rc]\nSee ' -h' for help and examples")
|
||||
}
|
||||
|
||||
func validateApplyArgs(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
|
||||
func TestAlphaEnablement(t *testing.T) {
|
||||
alphas := map[cmdutil.FeatureGate]string{
|
||||
cmdutil.ApplySet: "applyset",
|
||||
}
|
||||
for feature, flag := range alphas {
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
flags := NewApplyFlags(genericclioptions.NewTestIOStreamsDiscard())
|
||||
flags.AddFlags(cmd)
|
||||
require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature)
|
||||
|
||||
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
flags := NewApplyFlags(genericclioptions.NewTestIOStreamsDiscard())
|
||||
flags.AddFlags(cmd)
|
||||
require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestApplyFlagValidation(t *testing.T) {
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
tests := []struct {
|
||||
args [][]string
|
||||
expectedErr string
|
||||
args [][]string
|
||||
enableAlphas []cmdutil.FeatureGate
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
args: [][]string{
|
||||
@ -147,6 +164,16 @@ func TestApplyFlagValidation(t *testing.T) {
|
||||
},
|
||||
expectedErr: "--force cannot be used with --prune",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "true"},
|
||||
{"force", "true"},
|
||||
{"applyset", "mySecret"},
|
||||
{"namespace", "myNs"},
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
|
||||
expectedErr: "--force cannot be used with --prune",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"server-side", "true"},
|
||||
@ -155,27 +182,91 @@ func TestApplyFlagValidation(t *testing.T) {
|
||||
},
|
||||
expectedErr: "--prune is in alpha and doesn't currently work on objects created by server-side apply",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "true"},
|
||||
},
|
||||
expectedErr: "all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "false"},
|
||||
{"applyset", "mySecret"},
|
||||
{"namespace", "myNs"},
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
|
||||
expectedErr: "--applyset requires --prune",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "true"},
|
||||
{"applyset", "mySecret"},
|
||||
{"selector", "foo=bar"},
|
||||
{"namespace", "myNs"},
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
|
||||
expectedErr: "--selector is incompatible with --applyset",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "true"},
|
||||
{"applyset", "mySecret"},
|
||||
{"namespace", "myNs"},
|
||||
{"all", "true"},
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
|
||||
expectedErr: "--all is incompatible with --applyset",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "true"},
|
||||
{"applyset", "mySecret"},
|
||||
{"namespace", "myNs"},
|
||||
{"prune-allowlist", "core/v1/ConfigMap"},
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
|
||||
expectedErr: "--prune-allowlist is incompatible with --applyset",
|
||||
},
|
||||
{
|
||||
args: [][]string{
|
||||
{"prune", "true"},
|
||||
{"applyset", "foo"},
|
||||
{"namespace", "myNs"},
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
|
||||
expectedErr: "--applyset is not yet supported",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cmd := &cobra.Command{}
|
||||
flags := NewApplyFlags(genericclioptions.NewTestIOStreamsDiscard())
|
||||
flags.AddFlags(cmd)
|
||||
cmd.Flags().Set("filename", "unused")
|
||||
for _, arg := range test.args {
|
||||
cmd.Flags().Set(arg[0], arg[1])
|
||||
}
|
||||
o, err := flags.ToOptions(f, cmd, "kubectl", []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating apply options: %s", err)
|
||||
}
|
||||
err = o.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("missing expected error")
|
||||
}
|
||||
if test.expectedErr != err.Error() {
|
||||
t.Errorf("expected error %s, got %s", test.expectedErr, err)
|
||||
}
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
flags := NewApplyFlags(genericclioptions.NewTestIOStreamsDiscard())
|
||||
flags.AddFlags(cmd)
|
||||
cmd.Flags().Set("filename", "unused")
|
||||
for _, arg := range test.args {
|
||||
if arg[0] == "namespace" {
|
||||
f.WithNamespace(arg[1])
|
||||
} else {
|
||||
cmd.Flags().Set(arg[0], arg[1])
|
||||
}
|
||||
}
|
||||
o, err := flags.ToOptions(f, cmd, "kubectl", []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating apply options: %s", err)
|
||||
}
|
||||
err = o.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("missing expected error for case %d with args %+v", i, test.args)
|
||||
}
|
||||
if test.expectedErr != err.Error() {
|
||||
t.Errorf("expected error %s, got %s", test.expectedErr, err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1990,3 +2081,134 @@ func TestDontAllowApplyWithPodGeneratedName(t *testing.T) {
|
||||
cmd.Flags().Set("dry-run", "client")
|
||||
cmd.Run(cmd, []string{})
|
||||
}
|
||||
|
||||
func TestApplySetParentValidation(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
applysetFlag string
|
||||
namespaceFlag string
|
||||
setup func(*testing.T, *cmdtesting.TestFactory)
|
||||
expectParentKind string
|
||||
expectBlankParentNs bool
|
||||
expectErr string
|
||||
}{
|
||||
"parent type must be valid": {
|
||||
applysetFlag: "doesnotexist/thename",
|
||||
expectErr: "invalid parent reference \"doesnotexist/thename\": no matches for /, Resource=doesnotexist",
|
||||
},
|
||||
"parent name must be present": {
|
||||
applysetFlag: "secret/",
|
||||
expectErr: "invalid parent reference \"secret/\": name cannot be blank",
|
||||
},
|
||||
"configmap parents are valid": {
|
||||
applysetFlag: "configmap/thename",
|
||||
namespaceFlag: "mynamespace",
|
||||
expectParentKind: "ConfigMap",
|
||||
},
|
||||
"secret parents are valid": {
|
||||
applysetFlag: "secret/thename",
|
||||
namespaceFlag: "mynamespace",
|
||||
expectParentKind: "Secret",
|
||||
},
|
||||
"plural resource works": {
|
||||
applysetFlag: "secrets/thename",
|
||||
namespaceFlag: "mynamespace",
|
||||
expectParentKind: "Secret",
|
||||
},
|
||||
"other namespaced builtin parents types are correctly parsed but invalid": {
|
||||
applysetFlag: "deployments.apps/thename",
|
||||
expectParentKind: "Deployment",
|
||||
expectErr: "[resource \"apps/v1, Resource=deployments\" is not permitted as an ApplySet parent, namespace is required to use namespace-scoped ApplySet]",
|
||||
},
|
||||
"namespaced builtin parents with multi-segment groups are correctly parsed but invalid": {
|
||||
applysetFlag: "priorityclasses.scheduling.k8s.io/thename",
|
||||
expectParentKind: "PriorityClass",
|
||||
expectErr: "resource \"scheduling.k8s.io/v1alpha1, Resource=priorityclasses\" is not permitted as an ApplySet parent",
|
||||
},
|
||||
"non-namespaced builtin types are correctly parsed but invalid": {
|
||||
applysetFlag: "namespaces/thename",
|
||||
expectParentKind: "Namespace",
|
||||
namespaceFlag: "somenamespace",
|
||||
expectBlankParentNs: true,
|
||||
expectErr: "resource \"/v1, Resource=namespaces\" is not permitted as an ApplySet parent",
|
||||
},
|
||||
"parent namespace should use the value of the namespace flag": {
|
||||
applysetFlag: "mysecret",
|
||||
namespaceFlag: "mynamespace",
|
||||
expectParentKind: "Secret",
|
||||
},
|
||||
"parent namespace should not use the default namespace from ClientConfig": {
|
||||
applysetFlag: "mysecret",
|
||||
setup: func(t *testing.T, f *cmdtesting.TestFactory) {
|
||||
// by default, the value "default" is used for the namespace
|
||||
// make sure this assumption still holds
|
||||
ns, overridden, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
require.NoError(t, err)
|
||||
require.Falsef(t, overridden, "namespace unexpectedly overridden")
|
||||
require.Equal(t, "default", ns)
|
||||
},
|
||||
expectBlankParentNs: true,
|
||||
expectParentKind: "Secret",
|
||||
expectErr: "namespace is required to use namespace-scoped ApplySet",
|
||||
},
|
||||
"parent namespace should not use the default namespace from the user's kubeconfig": {
|
||||
applysetFlag: "mysecret",
|
||||
setup: func(t *testing.T, f *cmdtesting.TestFactory) {
|
||||
kubeConfig := clientcmdapi.NewConfig()
|
||||
kubeConfig.CurrentContext = "default"
|
||||
kubeConfig.Contexts["default"] = &clientcmdapi.Context{Namespace: "bar"}
|
||||
clientConfig := clientcmd.NewDefaultClientConfig(*kubeConfig, &clientcmd.ConfigOverrides{
|
||||
ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"}})
|
||||
f.WithClientConfig(clientConfig)
|
||||
},
|
||||
expectBlankParentNs: true,
|
||||
expectParentKind: "Secret",
|
||||
expectErr: "namespace is required to use namespace-scoped ApplySet",
|
||||
},
|
||||
}
|
||||
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
flags := NewApplyFlags(genericclioptions.NewTestIOStreamsDiscard())
|
||||
flags.AddFlags(cmd)
|
||||
cmd.Flags().Set("filename", filenameRC)
|
||||
cmd.Flags().Set("applyset", test.applysetFlag)
|
||||
cmd.Flags().Set("prune", "true")
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
var expectedParentNs string
|
||||
if test.namespaceFlag != "" {
|
||||
f.WithNamespace(test.namespaceFlag)
|
||||
if !test.expectBlankParentNs {
|
||||
expectedParentNs = test.namespaceFlag
|
||||
}
|
||||
}
|
||||
|
||||
if test.setup != nil {
|
||||
test.setup(t, f)
|
||||
}
|
||||
|
||||
o, err := flags.ToOptions(f, cmd, "kubectl", []string{})
|
||||
if test.expectErr == "" {
|
||||
require.NoError(t, err, "ToOptions error")
|
||||
} else if err != nil {
|
||||
require.EqualError(t, err, test.expectErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedParentNs, o.ApplySet.ParentRef.Namespace)
|
||||
assert.Equal(t, test.expectParentKind, o.ApplySet.ParentRef.RESTMapping.GroupVersionKind.Kind)
|
||||
|
||||
err = o.Validate()
|
||||
if test.expectErr != "" {
|
||||
require.EqualError(t, err, test.expectErr)
|
||||
} else if err.Error() == "--applyset is not yet supported" {
|
||||
// TODO: remove this when the feature is complete
|
||||
} else {
|
||||
require.NoError(t, err, "Validate error")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
119
staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset.go
Normal file
119
staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset.go
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes 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 apply
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
)
|
||||
|
||||
var defaultApplySetParentGVR = schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
|
||||
|
||||
// ApplySet tracks the information about an applyset apply/prune
|
||||
type ApplySet struct {
|
||||
// ParentRef is the reference to the parent object that is used to track the applyset.
|
||||
ParentRef *ApplySetParentRef
|
||||
|
||||
// resources is the set of all the resources that (might) be part of this applyset.
|
||||
resources map[schema.GroupVersionResource]struct{}
|
||||
|
||||
// namespaces is the set of all namespaces that (might) contain objects that are part of this applyset.
|
||||
namespaces map[string]struct{}
|
||||
}
|
||||
|
||||
var builtinApplySetParentGVRs = map[schema.GroupVersionResource]bool{
|
||||
defaultApplySetParentGVR: true,
|
||||
{Version: "v1", Resource: "configmaps"}: true,
|
||||
}
|
||||
|
||||
// ApplySetParentRef stores object and type meta for the parent object that is used to track the applyset.
|
||||
type ApplySetParentRef struct {
|
||||
Name string
|
||||
Namespace string
|
||||
RESTMapping *meta.RESTMapping
|
||||
}
|
||||
|
||||
// NewApplySet creates a new ApplySet object from a parent reference in the format [RESOURCE][.GROUP]/NAME
|
||||
func NewApplySet(parentRefStr string, nsFromFlag string, mapper meta.RESTMapper) (*ApplySet, error) {
|
||||
parent, err := parentRefFromStr(parentRefStr, mapper)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid parent reference %q: %w", parentRefStr, err)
|
||||
}
|
||||
if parent.IsNamespaced() {
|
||||
parent.Namespace = nsFromFlag
|
||||
}
|
||||
return &ApplySet{
|
||||
resources: make(map[schema.GroupVersionResource]struct{}),
|
||||
namespaces: make(map[string]struct{}),
|
||||
ParentRef: parent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ID is the label value that we are using to identify this applyset.
|
||||
func (a ApplySet) ID() string {
|
||||
// TODO: base64(sha256(gknn))
|
||||
return "placeholder-todo"
|
||||
}
|
||||
|
||||
// Validate imposes restrictions on the parent object that is used to track the applyset.
|
||||
func (a ApplySet) Validate() error {
|
||||
var errors []error
|
||||
// TODO: permit CRDs that have the annotation required by the ApplySet specification
|
||||
if !builtinApplySetParentGVRs[a.ParentRef.RESTMapping.Resource] {
|
||||
errors = append(errors, fmt.Errorf("resource %q is not permitted as an ApplySet parent", a.ParentRef.RESTMapping.Resource))
|
||||
}
|
||||
if a.ParentRef.IsNamespaced() && a.ParentRef.Namespace == "" {
|
||||
errors = append(errors, fmt.Errorf("namespace is required to use namespace-scoped ApplySet"))
|
||||
}
|
||||
return utilerrors.NewAggregate(errors)
|
||||
}
|
||||
|
||||
func (p *ApplySetParentRef) IsNamespaced() bool {
|
||||
return p.RESTMapping.Scope.Name() == meta.RESTScopeNameNamespace
|
||||
}
|
||||
|
||||
// parentRefFromStr creates a new ApplySetParentRef from a parent reference in the format [RESOURCE][.GROUP]/NAME
|
||||
func parentRefFromStr(parentRefStr string, mapper meta.RESTMapper) (*ApplySetParentRef, error) {
|
||||
var gvr schema.GroupVersionResource
|
||||
var name string
|
||||
|
||||
if groupRes, nameSuffix, hasTypeInfo := strings.Cut(parentRefStr, "/"); hasTypeInfo {
|
||||
name = nameSuffix
|
||||
gvr = schema.ParseGroupResource(groupRes).WithVersion("")
|
||||
} else {
|
||||
name = parentRefStr
|
||||
gvr = defaultApplySetParentGVR
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("name cannot be blank")
|
||||
}
|
||||
|
||||
gvk, err := mapper.KindFor(gvr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := mapper.RESTMapping(gvk.GroupKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ApplySetParentRef{Name: name, RESTMapping: mapping}, nil
|
||||
}
|
30
staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset_pruner.go
Normal file
30
staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset_pruner.go
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes 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 apply
|
||||
|
||||
import "fmt"
|
||||
|
||||
type applySetPruner struct {
|
||||
}
|
||||
|
||||
func newApplySetPruner(_ *ApplyOptions) *applySetPruner {
|
||||
return &applySetPruner{}
|
||||
}
|
||||
|
||||
func (p *applySetPruner) pruneAll() error {
|
||||
return fmt.Errorf("ApplySet-based pruning is not yet implemented")
|
||||
}
|
@ -18,7 +18,6 @@ package explain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@ -82,7 +81,7 @@ func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *Expl
|
||||
return &ExplainOptions{
|
||||
IOStreams: streams,
|
||||
CmdParent: parent,
|
||||
EnableOpenAPIV3: os.Getenv("KUBECTL_EXPLAIN_OPENAPIV3") == "true",
|
||||
EnableOpenAPIV3: cmdutil.ExplainOpenapiV3.IsEnabled(),
|
||||
OutputFormat: "plaintext",
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +21,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
)
|
||||
|
||||
@ -150,3 +152,21 @@ func TestExplain(t *testing.T) {
|
||||
t.Fatalf("expected output should include pod batch/v1beta1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphaEnablement(t *testing.T) {
|
||||
alphas := map[cmdutil.FeatureGate]string{
|
||||
cmdutil.ExplainOpenapiV3: "output",
|
||||
}
|
||||
for feature, flag := range alphas {
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
cmd := NewCmdExplain("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
||||
require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature)
|
||||
|
||||
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) {
|
||||
cmd := NewCmdExplain("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
||||
require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,10 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
@ -178,3 +180,18 @@ func InitTestErrorHandler(t *testing.T) {
|
||||
t.Errorf("Error running command (exit code %d): %s", code, str)
|
||||
})
|
||||
}
|
||||
|
||||
// WithAlphaEnvs calls func f with the given env-var-based feature gates enabled,
|
||||
// and then restores the original values of those variables.
|
||||
func WithAlphaEnvs(features []cmdutil.FeatureGate, t *testing.T, f func(*testing.T)) {
|
||||
for _, feature := range features {
|
||||
key := string(feature)
|
||||
if key != "" {
|
||||
oldValue := os.Getenv(key)
|
||||
err := os.Setenv(key, "true")
|
||||
require.NoError(t, err, "unexpected error setting alpha env")
|
||||
defer os.Setenv(key, oldValue)
|
||||
}
|
||||
}
|
||||
f(t)
|
||||
}
|
||||
|
@ -422,6 +422,17 @@ func GetPodRunningTimeoutFlag(cmd *cobra.Command) (time.Duration, error) {
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
type FeatureGate string
|
||||
|
||||
const (
|
||||
ApplySet FeatureGate = "KUBECTL_APPLYSET"
|
||||
ExplainOpenapiV3 FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3"
|
||||
)
|
||||
|
||||
func (f FeatureGate) IsEnabled() bool {
|
||||
return os.Getenv(string(f)) == "true"
|
||||
}
|
||||
|
||||
func AddValidateFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String(
|
||||
"validate",
|
||||
@ -499,6 +510,23 @@ func AddLabelSelectorFlagVar(cmd *cobra.Command, p *string) {
|
||||
cmd.Flags().StringVarP(p, "selector", "l", *p, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
|
||||
}
|
||||
|
||||
func AddPruningFlags(cmd *cobra.Command, prune *bool, pruneAllowlist *[]string, pruneWhitelist *[]string, all *bool, applySetRef *string) {
|
||||
// Flags associated with the original allowlist-based alpha
|
||||
cmd.Flags().StringArrayVar(pruneAllowlist, "prune-allowlist", *pruneAllowlist, "Overwrite the default allowlist with <group/version/kind> for --prune")
|
||||
cmd.Flags().StringArrayVar(pruneWhitelist, "prune-whitelist", *pruneWhitelist, "Overwrite the default whitelist with <group/version/kind> for --prune") // TODO: Remove this in kubectl 1.28 or later
|
||||
_ = cmd.Flags().MarkDeprecated("prune-whitelist", "Use --prune-allowlist instead.")
|
||||
cmd.Flags().BoolVar(all, "all", *all, "Select all resources in the namespace of the specified resource types.")
|
||||
|
||||
// Flags associated with the new ApplySet-based alpha
|
||||
if ApplySet.IsEnabled() {
|
||||
cmd.Flags().StringVar(applySetRef, "applyset", *applySetRef, "[alpha] The name of the ApplySet that tracks which resources are being managed, for the purposes of determining what to prune. Live resources that are part of the ApplySet but have been removed from the provided configs will be deleted. Format: [RESOURCE][.GROUP]/NAME. A Secret will be used if no resource or group is specified.")
|
||||
cmd.Flags().BoolVar(prune, "prune", *prune, "Automatically delete previously applied resource objects that do not appear in the provided configs. For alpha1, use with either -l or --all. For alpha2, use with --applyset.")
|
||||
} else {
|
||||
// different docs for the shared --prune flag if only alpha1 is enabled
|
||||
cmd.Flags().BoolVar(prune, "prune", *prune, "Automatically delete resource objects, that do not appear in the configs and are created by either apply or create --save-config. Should be used with either -l or --all.")
|
||||
}
|
||||
}
|
||||
|
||||
func AddSubresourceFlags(cmd *cobra.Command, subresource *string, usage string, allowedSubresources ...string) {
|
||||
cmd.Flags().StringVar(subresource, "subresource", "", fmt.Sprintf("%s Must be one of %v. This flag is alpha and may change in the future.", usage, allowedSubresources))
|
||||
CheckErr(cmd.RegisterFlagCompletionFunc("subresource", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
|
Loading…
Reference in New Issue
Block a user