Merge pull request #11745 from johngmyers/remove-option

Allow unsetting fields from the command line
This commit is contained in:
Kubernetes Prow Robot 2021-06-14 09:31:04 -07:00 committed by GitHub
commit bfd0b6d9ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1333 additions and 9 deletions

3
cmd/kops/BUILD.bazel generated
View File

@ -50,6 +50,9 @@ go_library(
"toolbox_dump.go",
"toolbox_instance_selector.go",
"toolbox_template.go",
"unset.go",
"unset_cluster.go",
"unset_instancegroups.go",
"update.go",
"update_cluster.go",
"upgrade.go",

View File

@ -70,8 +70,10 @@ type CreateClusterOptions struct {
// SSHPublicKeys is a map of the SSH public keys we should configure; required on AWS, not required on GCE
SSHPublicKeys map[string][]byte
// Overrides allows settings values direct in the spec
// Overrides allows setting values directly in the spec.
Overrides []string
// Unsets allows unsetting values directly in the spec.
Unsets []string
// CloudLabels are cloud-provider-level tags for instance groups and volumes.
CloudLabels string
@ -301,6 +303,7 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {
if featureflag.SpecOverrideFlag.Enabled() {
cmd.Flags().StringSliceVar(&options.Overrides, "override", options.Overrides, "Directly configure values in the spec")
cmd.Flags().StringSliceVar(&options.Unsets, "unset", options.Unsets, "Directly unset values in the spec")
}
// GCE flags
@ -510,6 +513,9 @@ func RunCreateCluster(ctx context.Context, f *util.Factory, out io.Writer, c *Cr
cluster.Spec.MasterPublicName = c.MasterPublicName
}
if err := commands.UnsetClusterFields(c.Unsets, cluster); err != nil {
return err
}
if err := commands.SetClusterFields(c.Overrides, cluster); err != nil {
return err
}

View File

@ -150,6 +150,7 @@ func NewCmdRoot(f *util.Factory, out io.Writer) *cobra.Command {
cmd.AddCommand(NewCmdRollingUpdate(f, out))
cmd.AddCommand(NewCmdSet(f, out))
cmd.AddCommand(NewCmdToolbox(f, out))
cmd.AddCommand(NewCmdUnset(f, out))
cmd.AddCommand(NewCmdValidate(f, out))
cmd.AddCommand(NewCmdVersion(f, out))

View File

@ -34,7 +34,7 @@ var (
setExample = templates.Examples(i18n.T(`
# Set cluster to run kubernetes version 1.17.0
kops set cluster k8s-cluster.example.com spec.kubernetesVersion=1.17.0
kops set instancegroup --name k8s-cluster.example.com nodes spec.maxSize=4
kops set instancegroup --name k8s-cluster.example.com nodes-1a spec.maxSize=4
`))
)

53
cmd/kops/unset.go Normal file
View File

@ -0,0 +1,53 @@
/*
Copyright 2021 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 main
import (
"io"
"github.com/spf13/cobra"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
var (
unsetLong = templates.LongDesc(i18n.T(`Unset a configuration field.
kops unset does not update the cloud resources; to apply the changes use "kops update cluster".
`))
unsetExample = templates.Examples(i18n.T(`
kops unset cluster k8s-cluster.example.com spec.iam.allowContainerRegistry
kops unset instancegroup --name k8s-cluster.example.com nodes-1a spec.maxSize
`))
)
func NewCmdUnset(f *util.Factory, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "unset",
Short: i18n.T("Unset fields on clusters and other resources."),
Long: unsetLong,
Example: unsetExample,
}
// create subcommands
cmd.AddCommand(NewCmdUnsetCluster(f, out))
cmd.AddCommand(NewCmdUnsetInstancegroup(f, out))
return cmd
}

75
cmd/kops/unset_cluster.go Normal file
View File

@ -0,0 +1,75 @@
/*
Copyright 2019 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 main
import (
"context"
"io"
"strings"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kops/pkg/commands"
)
var (
unsetClusterLong = templates.LongDesc(i18n.T(`Unset a cluster field value.
This command changes the desired cluster configuration in the registry.
kops unset does not update the cloud resources; to apply the changes use "kops update cluster".`))
unsetClusterExample = templates.Examples(i18n.T(`
kops unset cluster k8s-cluster.example.com spec.iam.allowContainerRegistry
`))
)
// NewCmdUnsetCluster builds a cobra command for the kops unset cluster command
func NewCmdUnsetCluster(f *util.Factory, out io.Writer) *cobra.Command {
options := &commands.UnsetClusterOptions{}
cmd := &cobra.Command{
Use: "cluster",
Short: i18n.T("Unset cluster fields."),
Long: unsetClusterLong,
Example: unsetClusterExample,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.TODO()
for i, arg := range args {
if i == 0 && !strings.HasPrefix(arg, "spec.") && !strings.HasPrefix(arg, "cluster.") {
options.ClusterName = arg
} else {
options.Fields = append(options.Fields, arg)
}
}
if options.ClusterName == "" {
options.ClusterName = rootCommand.ClusterName()
}
if err := commands.RunUnsetCluster(ctx, f, cmd, out, options); err != nil {
exitWithError(err)
}
},
}
return cmd
}

View File

@ -0,0 +1,75 @@
/*
Copyright 2021 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 main
import (
"context"
"io"
"strings"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kops/pkg/commands"
)
var (
unsetInstancegroupLong = templates.LongDesc(i18n.T(`Unset an instance group field value.
This command changes the desired instance group configuration in the registry.
kops unset does not update the cloud resources; to apply the changes use "kops update cluster".`))
unsetInstancegroupExample = templates.Examples(i18n.T(`
# Set instance group to run default image
kops unset instancegroup --name k8s-cluster.example.com nodes spec.image
`))
)
// NewCmdUnsetInstancegroup builds a cobra command for the kops set instancegroup command.
func NewCmdUnsetInstancegroup(f *util.Factory, out io.Writer) *cobra.Command {
options := &commands.UnsetInstanceGroupOptions{}
cmd := &cobra.Command{
Use: "instancegroup",
Aliases: []string{"instancegroups", "ig"},
Short: i18n.T("Unset instancegroup fields."),
Long: unsetInstancegroupLong,
Example: unsetInstancegroupExample,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.TODO()
for i, arg := range args {
if i == 0 && !strings.HasPrefix(arg, "spec.") && !strings.HasPrefix(arg, "instancegroup.") {
options.InstanceGroupName = arg
} else {
options.Fields = append(options.Fields, arg)
}
}
options.ClusterName = rootCommand.ClusterName()
if err := commands.RunUnsetInstancegroup(ctx, f, cmd, out, options); err != nil {
exitWithError(err)
}
},
}
return cmd
}

1
docs/cli/kops.md generated
View File

@ -49,6 +49,7 @@ kOps is Kubernetes Operations.
* [kops rolling-update](kops_rolling-update.md) - Rolling update a cluster.
* [kops set](kops_set.md) - Set fields on clusters and other resources.
* [kops toolbox](kops_toolbox.md) - Misc infrequently used commands.
* [kops unset](kops_unset.md) - Unset fields on clusters and other resources.
* [kops update](kops_update.md) - Update a cluster.
* [kops upgrade](kops_upgrade.md) - Upgrade a kubernetes cluster.
* [kops validate](kops_validate.md) - Validate a kOps cluster.

2
docs/cli/kops_set.md generated
View File

@ -16,7 +16,7 @@ Set a configuration field.
```
# Set cluster to run kubernetes version 1.17.0
kops set cluster k8s-cluster.example.com spec.kubernetesVersion=1.17.0
kops set instancegroup --name k8s-cluster.example.com nodes spec.maxSize=4
kops set instancegroup --name k8s-cluster.example.com nodes-1a spec.maxSize=4
```
### Options

53
docs/cli/kops_unset.md generated Normal file
View File

@ -0,0 +1,53 @@
<!--- This file is automatically generated by make gen-cli-docs; changes should be made in the go CLI command code (under cmd/kops) -->
## kops unset
Unset fields on clusters and other resources.
### Synopsis
Unset a configuration field.
kops unset does not update the cloud resources; to apply the changes use "kops update cluster".
### Examples
```
kops unset cluster k8s-cluster.example.com spec.iam.allowContainerRegistry
kops unset instancegroup --name k8s-cluster.example.com nodes-1a spec.maxSize
```
### Options
```
-h, --help help for unset
```
### Options inherited from parent commands
```
--add_dir_header If true, adds the file directory to the header of the log messages
--alsologtostderr log to standard error as well as files
--config string yaml config file (default is $HOME/.kops.yaml)
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--name string Name of cluster. Overrides KOPS_CLUSTER_NAME environment variable
--one_output If true, only write logs to their native severity level (vs also writing to each lower severity level)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--state string Location of state storage (kops 'config' file). Overrides KOPS_STATE_STORE environment variable
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
### SEE ALSO
* [kops](kops.md) - kOps is Kubernetes Operations.
* [kops unset cluster](kops_unset_cluster.md) - Unset cluster fields.
* [kops unset instancegroup](kops_unset_instancegroup.md) - Unset instancegroup fields.

56
docs/cli/kops_unset_cluster.md generated Normal file
View File

@ -0,0 +1,56 @@
<!--- This file is automatically generated by make gen-cli-docs; changes should be made in the go CLI command code (under cmd/kops) -->
## kops unset cluster
Unset cluster fields.
### Synopsis
Unset a cluster field value.
This command changes the desired cluster configuration in the registry.
kops unset does not update the cloud resources; to apply the changes use "kops update cluster".
```
kops unset cluster [flags]
```
### Examples
```
kops unset cluster k8s-cluster.example.com spec.iam.allowContainerRegistry
```
### Options
```
-h, --help help for cluster
```
### Options inherited from parent commands
```
--add_dir_header If true, adds the file directory to the header of the log messages
--alsologtostderr log to standard error as well as files
--config string yaml config file (default is $HOME/.kops.yaml)
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--name string Name of cluster. Overrides KOPS_CLUSTER_NAME environment variable
--one_output If true, only write logs to their native severity level (vs also writing to each lower severity level)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--state string Location of state storage (kops 'config' file). Overrides KOPS_STATE_STORE environment variable
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
### SEE ALSO
* [kops unset](kops_unset.md) - Unset fields on clusters and other resources.

57
docs/cli/kops_unset_instancegroup.md generated Normal file
View File

@ -0,0 +1,57 @@
<!--- This file is automatically generated by make gen-cli-docs; changes should be made in the go CLI command code (under cmd/kops) -->
## kops unset instancegroup
Unset instancegroup fields.
### Synopsis
Unset an instance group field value.
This command changes the desired instance group configuration in the registry.
kops unset does not update the cloud resources; to apply the changes use "kops update cluster".
```
kops unset instancegroup [flags]
```
### Examples
```
# Set instance group to run default image
kops unset instancegroup --name k8s-cluster.example.com nodes spec.image
```
### Options
```
-h, --help help for instancegroup
```
### Options inherited from parent commands
```
--add_dir_header If true, adds the file directory to the header of the log messages
--alsologtostderr log to standard error as well as files
--config string yaml config file (default is $HOME/.kops.yaml)
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--name string Name of cluster. Overrides KOPS_CLUSTER_NAME environment variable
--one_output If true, only write logs to their native severity level (vs also writing to each lower severity level)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--state string Location of state storage (kops 'config' file). Overrides KOPS_STATE_STORE environment variable
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
### SEE ALSO
* [kops unset](kops_unset.md) - Unset fields on clusters and other resources.

View File

@ -7,6 +7,8 @@ go_library(
"helpers_readwrite.go",
"set_cluster.go",
"set_instancegroups.go",
"unset_cluster.go",
"unset_instancegroups.go",
"version.go",
],
importpath = "k8s.io/kops/pkg/commands",
@ -35,6 +37,8 @@ go_test(
srcs = [
"set_cluster_test.go",
"set_instancegroups_test.go",
"unset_cluster_test.go",
"unset_instancegroups_test.go",
],
embed = [":go_default_library"],
deps = [

View File

@ -0,0 +1,85 @@
/*
Copyright 2021 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 commands
import (
"context"
"fmt"
"io"
"strings"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kops/cmd/kops/util"
api "k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/util/pkg/reflectutils"
)
type UnsetClusterOptions struct {
Fields []string
ClusterName string
}
// RunUnsetCluster implements the unset cluster command logic
func RunUnsetCluster(ctx context.Context, f *util.Factory, cmd *cobra.Command, out io.Writer, options *UnsetClusterOptions) error {
if !featureflag.SpecOverrideFlag.Enabled() {
return fmt.Errorf("unset cluster command is current feature gated; set `export KOPS_FEATURE_FLAGS=SpecOverrideFlag`")
}
if options.ClusterName == "" {
return field.Required(field.NewPath("clusterName"), "Cluster name is required")
}
clientset, err := f.Clientset()
if err != nil {
return err
}
cluster, err := clientset.GetCluster(ctx, options.ClusterName)
if err != nil {
return err
}
instanceGroups, err := ReadAllInstanceGroups(ctx, clientset, cluster)
if err != nil {
return err
}
if err := UnsetClusterFields(options.Fields, cluster); err != nil {
return err
}
if err := UpdateCluster(ctx, clientset, cluster, instanceGroups); err != nil {
return err
}
return nil
}
// UnsetClusterFields unsets field values in the cluster
func UnsetClusterFields(fields []string, cluster *api.Cluster) error {
for _, field := range fields {
key := strings.TrimPrefix(field, "cluster.")
if err := reflectutils.Unset(cluster, key); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,455 @@
/*
Copyright 2021 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 commands
import (
"reflect"
"testing"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/upup/pkg/fi"
)
func TestUnsetClusterBadInput(t *testing.T) {
fields := []string{
"bad-unset-input",
}
err := UnsetClusterFields(fields, &kops.Cluster{})
if err == nil {
t.Errorf("expected a field parsing error, but received none")
}
}
func TestUnsetClusterFields(t *testing.T) {
grid := []struct {
Fields []string
Input kops.Cluster
Output kops.Cluster
}{
{
Fields: []string{
"spec.kubernetesVersion",
"spec.kubelet.authorizationMode",
"spec.kubelet.authenticationTokenWebhook",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
KubernetesVersion: "1.8.2",
Kubelet: &kops.KubeletConfigSpec{
AuthorizationMode: "Webhook",
AuthenticationTokenWebhook: fi.Bool(true),
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Kubelet: &kops.KubeletConfigSpec{},
},
},
},
{
Fields: []string{"spec.kubelet.authorizationMode"},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Kubelet: &kops.KubeletConfigSpec{
AuthorizationMode: "Webhook",
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Kubelet: &kops.KubeletConfigSpec{},
},
},
},
{
Fields: []string{"spec.kubelet.authenticationTokenWebhook"},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Kubelet: &kops.KubeletConfigSpec{
AuthenticationTokenWebhook: fi.Bool(false),
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Kubelet: &kops.KubeletConfigSpec{},
},
},
},
{
Fields: []string{"spec.docker.selinuxEnabled"},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Docker: &kops.DockerConfig{
SelinuxEnabled: fi.Bool(true),
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Docker: &kops.DockerConfig{},
},
},
},
{
Fields: []string{"spec.kubernetesVersion"},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
KubernetesVersion: "v1.2.3",
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{},
},
},
{
Fields: []string{"spec.masterPublicName"},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
MasterPublicName: "api.example.com",
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{},
},
},
{
Fields: []string{"spec.kubeDNS.provider"},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
KubeDNS: &kops.KubeDNSConfig{
Provider: "CoreDNS",
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
KubeDNS: &kops.KubeDNSConfig{},
},
},
},
{
Fields: []string{
"cluster.spec.nodePortAccess",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
NodePortAccess: []string{"10.0.0.0/8", "192.168.0.0/16"},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{},
},
},
{
Fields: []string{
"cluster.spec.etcdClusters[*].enableEtcdTLS",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one", EnableEtcdTLS: true},
{Name: "two", EnableEtcdTLS: true},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one"},
{Name: "two"},
},
},
},
},
{
Fields: []string{
"cluster.spec.etcdClusters",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one", EnableTLSAuth: true},
{Name: "two", EnableTLSAuth: true},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{},
},
},
{
Fields: []string{
"cluster.spec.etcdClusters[*].version",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one", Version: "v3.2.1"},
{Name: "two", Version: "v3.2.1"},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one"},
{Name: "two"},
},
},
},
},
{
Fields: []string{
"cluster.spec.etcdClusters[*].provider",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one", Provider: kops.EtcdProviderTypeManager},
{Name: "two", Provider: kops.EtcdProviderTypeManager},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one"},
{Name: "two"},
},
},
},
},
{
Fields: []string{
"cluster.spec.etcdClusters[*]",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{Name: "one", Image: "etcd-manager:v1.2.3"},
{Name: "two", Image: "etcd-manager:v1.2.3"},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
EtcdClusters: []kops.EtcdClusterSpec{
{},
{},
},
},
},
},
{
Fields: []string{
"cluster.spec.networking.cilium.ipam",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{
Ipam: "on",
},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{},
},
},
},
},
{
Fields: []string{
"cluster.spec.networking.cilium.enableHostReachableServices",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{
EnableHostReachableServices: true,
},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{},
},
},
},
},
{
Fields: []string{
"cluster.spec.networking.cilium.enableNodePort",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{
EnableNodePort: true,
},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{},
},
},
},
},
{
Fields: []string{
"cluster.spec.networking.cilium.disableMasquerade",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{
DisableMasquerade: fi.Bool(true),
},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{},
},
},
},
},
{
Fields: []string{
"cluster.spec.kubeProxy.enabled",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
KubeProxy: &kops.KubeProxyConfig{
Enabled: fi.Bool(true),
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
KubeProxy: &kops.KubeProxyConfig{},
},
},
},
{
Fields: []string{
"cluster.spec.networking.cilium.agentPrometheusPort",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{
AgentPrometheusPort: 1234,
},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{},
},
},
},
},
}
for _, g := range grid {
c := g.Input
err := UnsetClusterFields(g.Fields, &c)
if err != nil {
t.Errorf("unexpected error from unsetClusterFields %v: %v", g.Fields, err)
continue
}
if !reflect.DeepEqual(c, g.Output) {
t.Errorf("unexpected output from unsetClusterFields %v. expected=%v, actual=%v", g.Fields, g.Output, c)
continue
}
}
}
func TestUnsetCiliumFields(t *testing.T) {
grid := []struct {
Fields []string
Input kops.Cluster
Output kops.Cluster
}{
{
Fields: []string{
"cluster.spec.networking.cilium.ipam",
"cluster.spec.networking.cilium.enableNodePort",
"cluster.spec.networking.cilium.disableMasquerade",
"cluster.spec.kubeProxy.enabled",
},
Input: kops.Cluster{
Spec: kops.ClusterSpec{
KubeProxy: &kops.KubeProxyConfig{
Enabled: fi.Bool(false),
},
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{
Ipam: "eni",
EnableNodePort: true,
DisableMasquerade: fi.Bool(true),
},
},
},
},
Output: kops.Cluster{
Spec: kops.ClusterSpec{
KubeProxy: &kops.KubeProxyConfig{},
Networking: &kops.NetworkingSpec{
Cilium: &kops.CiliumNetworkingSpec{},
},
},
},
},
}
for _, g := range grid {
c := g.Input
err := UnsetClusterFields(g.Fields, &c)
if err != nil {
t.Errorf("unexpected error from unsetClusterFields %v: %v", g.Fields, err)
continue
}
if !reflect.DeepEqual(c, g.Output) {
t.Errorf("unexpected output from unsetClusterFields %v. expected=%v, actual=%v", g.Fields, g.Output, c)
continue
}
}
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2021 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 commands
import (
"context"
"fmt"
"io"
"strings"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kops/cmd/kops/util"
api "k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/util/pkg/reflectutils"
)
// UnsetInstanceGroupOptions contains the options for unsetting configuration on an
// instance group.
type UnsetInstanceGroupOptions struct {
Fields []string
ClusterName string
InstanceGroupName string
}
// RunUnsetInstancegroup implements the unset instancegroup command logic.
func RunUnsetInstancegroup(ctx context.Context, f *util.Factory, cmd *cobra.Command, out io.Writer, options *UnsetInstanceGroupOptions) error {
if !featureflag.SpecOverrideFlag.Enabled() {
return fmt.Errorf("unset instancegroup is currently feature gated; set `export KOPS_FEATURE_FLAGS=SpecOverrideFlag`")
}
if options.ClusterName == "" {
return field.Required(field.NewPath("clusterName"), "Cluster name is required")
}
if options.InstanceGroupName == "" {
return field.Required(field.NewPath("instancegroupName"), "Instance Group name is required")
}
clientset, err := f.Clientset()
if err != nil {
return err
}
cluster, err := clientset.GetCluster(ctx, options.ClusterName)
if err != nil {
return err
}
instanceGroups, err := ReadAllInstanceGroups(ctx, clientset, cluster)
if err != nil {
return err
}
var instanceGroupToUpdate *api.InstanceGroup
for _, instanceGroup := range instanceGroups {
if instanceGroup.GetName() == options.InstanceGroupName {
instanceGroupToUpdate = instanceGroup
}
}
if instanceGroupToUpdate == nil {
return fmt.Errorf("unable to find instance group with name %q", options.InstanceGroupName)
}
err = UnsetInstancegroupFields(options.Fields, instanceGroupToUpdate)
if err != nil {
return err
}
err = UpdateInstanceGroup(ctx, clientset, cluster, instanceGroups, instanceGroupToUpdate)
if err != nil {
return err
}
return nil
}
// UnsetInstancegroupFields sets field values in the instance group.
func UnsetInstancegroupFields(fields []string, instanceGroup *api.InstanceGroup) error {
for _, field := range fields {
key := strings.TrimPrefix(field, "instancegroup.")
if err := reflectutils.Unset(instanceGroup, key); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,119 @@
/*
Copyright 2021 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 commands
import (
"reflect"
"testing"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/upup/pkg/fi"
)
func TestUnsetInstanceGroupsBadInput(t *testing.T) {
fields := []string{
"bad-unset-input",
}
err := UnsetInstancegroupFields(fields, &kops.InstanceGroup{})
if err == nil {
t.Errorf("expected a field parsing error, but received none")
}
}
func TestUnsetInstanceGroupsFields(t *testing.T) {
grid := []struct {
Fields []string
Input kops.InstanceGroup
Output kops.InstanceGroup
}{
{
Fields: []string{
"spec.image",
},
Input: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{
Image: "ami-test-1",
},
},
Output: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{},
},
},
{
Fields: []string{
"spec.machineType",
},
Input: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{
MachineType: "m5.large",
},
},
Output: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{},
},
},
{
Fields: []string{
"spec.minSize",
"spec.maxSize",
},
Input: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{
MinSize: fi.Int32(1),
MaxSize: fi.Int32(3),
},
},
Output: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{},
},
},
{
Fields: []string{
"spec.additionalSecurityGroups",
},
Input: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{
AdditionalSecurityGroups: []string{
"group1",
"group2",
"group3",
},
},
},
Output: kops.InstanceGroup{
Spec: kops.InstanceGroupSpec{},
},
},
}
for _, g := range grid {
ig := g.Input
err := UnsetInstancegroupFields(g.Fields, &ig)
if err != nil {
t.Errorf("unexpected error from unsetClusterFields %v: %v", g.Fields, err)
continue
}
if !reflect.DeepEqual(ig, g.Output) {
t.Errorf("unexpected output from unsetClusterFields %v. expected=%v, actual=%v", g.Fields, g.Output, ig)
continue
}
}
}

View File

@ -169,3 +169,43 @@ func setPrimitive(v reflect.Value, newValue string) error {
v.Set(newV)
return nil
}
func Unset(target interface{}, targetPath string) error {
targetValue := reflect.ValueOf(target)
targetFieldPath, err := ParseFieldPath(targetPath)
if err != nil {
return fmt.Errorf("cannot parse field path %q: %w", targetPath, err)
}
var fieldUnset = false
visitor := func(path *FieldPath, field *reflect.StructField, v reflect.Value) error {
if !targetFieldPath.HasPrefixMatch(path) {
return nil
}
if targetFieldPath.Matches(path) {
if !v.CanSet() {
return fmt.Errorf("cannot unset field %q (marked immutable)", path)
}
v.Set(reflect.Zero(v.Type()))
fieldUnset = true
return nil
}
return nil
}
err = ReflectRecursive(targetValue, visitor, &ReflectOptions{JSONNames: true})
if err != nil {
return err
}
if !fieldUnset {
return fmt.Errorf("field %s not found in %s", targetPath, BuildTypeName(reflect.TypeOf(target)))
}
return nil
}

View File

@ -207,7 +207,6 @@ func TestSetInvalidPath(t *testing.T) {
grid := []struct {
Name string
Input string
Expected string
Path string
Value string
ExpectedError string
@ -215,7 +214,6 @@ func TestSetInvalidPath(t *testing.T) {
{
Name: "setting with wildcard",
Input: "{ 'spec': { 'containers': [ {} ] } }",
Expected: "{ 'spec': { 'containers': [ { 'image': 'hello-world' } ] } }",
Path: "spec.containers[*].wrongImagePathName",
Value: "hello-world",
ExpectedError: "field spec.containers[*].wrongImagePathName not found in *fakeObject",
@ -223,7 +221,6 @@ func TestSetInvalidPath(t *testing.T) {
{
Name: "creating missing objects",
Input: "{ 'spec': { 'containers': [ {} ] } }",
Expected: "{ 'spec': { 'containers': [ { 'policy': { 'name': 'allowed' } } ] } }",
Path: "spec.containers[0].policy.wrongPolicyName",
Value: "allowed",
ExpectedError: "field spec.containers[0].policy.wrongPolicyName not found in *fakeObject",
@ -231,7 +228,6 @@ func TestSetInvalidPath(t *testing.T) {
{
Name: "set int",
Input: "{ 'spec': { 'containers': [ {} ] } }",
Expected: "{ 'spec': { 'containers': [ { 'int': 123 } ] } }",
Path: "spec.wrongNameContainers[0].int",
Value: "123",
ExpectedError: "field spec.wrongNameContainers[0].int not found in *fakeObject",
@ -239,7 +235,6 @@ func TestSetInvalidPath(t *testing.T) {
{
Name: "set int32",
Input: "{ 'spec': { 'containers': [ {} ] } }",
Expected: "{ 'spec': { 'containers': [ { 'int32': 123 } ] } }",
Path: "spec.containers[0].int32100",
Value: "123",
ExpectedError: "field spec.containers[0].int32100 not found in *fakeObject",
@ -247,7 +242,6 @@ func TestSetInvalidPath(t *testing.T) {
{
Name: "set int64",
Input: "{ 'spec': { 'containers': [ {} ] } }",
Expected: "{ 'spec': { 'containers': [ { 'int64': 123 } ] } }",
Path: "wrong.path.check",
Value: "123",
ExpectedError: "field wrong.path.check not found in *fakeObject",
@ -276,3 +270,147 @@ func TestSetInvalidPath(t *testing.T) {
})
}
}
func TestUnset(t *testing.T) {
grid := []struct {
Name string
Input string
Expected string
Path string
}{
{
Name: "simple unsetting",
Input: "{ 'spec': { 'containers': [ { 'image': 'hello-world' } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].image",
},
{
Name: "unsetting with wildcard",
Input: "{ 'spec': { 'containers': [ { 'image': 'hello-world' } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[*].image",
},
{
Name: "uset int",
Input: "{ 'spec': { 'containers': [ { 'int': 123 } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].int",
},
{
Name: "unset int32",
Input: "{ 'spec': { 'containers': [ { 'int32': 123 } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].int32",
},
{
Name: "unset int64",
Input: "{ 'spec': { 'containers': [ { 'int64': 123 } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].int64",
},
{
Name: "unset int pointer",
Input: "{ 'spec': { 'containers': [ { 'intPointer': 123 } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].intPointer",
},
{
Name: "unset int32 pointer",
Input: "{ 'spec': { 'containers': [ { 'int32Pointer': 123 } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].int32Pointer",
},
{
Name: "unset int64 pointer",
Input: "{ 'spec': { 'containers': [ { 'int64Pointer': 123 } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].int64Pointer",
},
{
Name: "unset enum",
Input: "{ 'spec': { 'containers': [ { 'enum': 'ABC' } ] } }",
Expected: "{ 'spec': { 'containers': [ { 'enum': ''} ] } }",
Path: "spec.containers[0].enum",
},
{
Name: "unset enum slice",
Input: "{ 'spec': { 'containers': [ { 'enumSlice': [ 'ABC', 'DEF' ] } ] } }",
Expected: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[0].enumSlice",
},
}
for _, g := range grid {
g := g
t.Run(g.Name, func(t *testing.T) {
c := &fakeObject{}
if err := json.Unmarshal(toJSON(g.Input), c); err != nil {
t.Fatalf("failed to unmarshal input: %v", err)
}
if err := Unset(c, g.Path); err != nil {
t.Fatalf("error from Unset: %v", err)
}
// Changed in-place
actual := c
expected := &fakeObject{}
if err := json.Unmarshal(toJSON(g.Expected), expected); err != nil {
t.Fatalf("failed to unmarshal expected: %v", err)
}
if !reflect.DeepEqual(c, expected) {
t.Fatalf("comparison failed; expected %+v, was %+v", expected, actual)
}
})
}
}
func TestUnsetInvalidPath(t *testing.T) {
grid := []struct {
Name string
Input string
Expected string
Path string
ExpectedError string
}{
{
Name: "usetting with wildcard",
Input: "{ 'spec': { 'containers': [ {} ] } }",
Path: "spec.containers[*].wrongImagePathName",
ExpectedError: "field spec.containers[*].wrongImagePathName not found in *fakeObject",
},
{
Name: "missing objects",
Input: "{ 'spec': { 'containers': [ { 'policy': { 'name': 'allowed' } } ] } }",
Path: "spec.containers[0].policy.wrongPolicyName",
ExpectedError: "field spec.containers[0].policy.wrongPolicyName not found in *fakeObject",
},
}
for _, g := range grid {
g := g
t.Run(g.Name, func(t *testing.T) {
c := &fakeObject{}
if err := json.Unmarshal(toJSON(g.Input), c); err != nil {
t.Fatalf("failed to unmarshal input: %v", err)
}
err := Unset(c, g.Path)
if err == nil {
t.Fatalf("Expected error for invalid path %s", g.Path)
}
if err.Error() != g.ExpectedError {
t.Fatalf("Expected Error: %s\n Actual Error: %s", g.ExpectedError, err.Error())
}
})
}
}