diff --git a/staging/src/k8s.io/controller-manager/BUILD b/staging/src/k8s.io/controller-manager/BUILD index efb9b6a45a4..3cd16902471 100644 --- a/staging/src/k8s.io/controller-manager/BUILD +++ b/staging/src/k8s.io/controller-manager/BUILD @@ -29,6 +29,7 @@ filegroup( "//staging/src/k8s.io/controller-manager/pkg/clientbuilder:all-srcs", "//staging/src/k8s.io/controller-manager/pkg/features:all-srcs", "//staging/src/k8s.io/controller-manager/pkg/informerfactory:all-srcs", + "//staging/src/k8s.io/controller-manager/pkg/leadermigration/config:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/BUILD b/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/BUILD new file mode 100644 index 00000000000..3decbc27c36 --- /dev/null +++ b/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/BUILD @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["config.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/controller-manager/pkg/leadermigration/config", + importpath = "k8s.io/controller-manager/pkg/leadermigration/config", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/controller-manager/config:go_default_library", + "//staging/src/k8s.io/controller-manager/config/v1alpha1:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["config_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/controller-manager/config:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/config.go b/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/config.go new file mode 100644 index 00000000000..8f460a30c93 --- /dev/null +++ b/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/config.go @@ -0,0 +1,99 @@ +/* +Copyright 2020 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 config + +import ( + "fmt" + "io/ioutil" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + util "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + internal "k8s.io/controller-manager/config" + "k8s.io/controller-manager/config/v1alpha1" +) + +// ResourceLockLeases is the resourceLock value for 'leases' API +const ResourceLockLeases = "leases" + +// ResourceLockEndpoints is the resourceLock value for 'endpoints' API +const ResourceLockEndpoints = "endpoints" + +var cfgScheme = runtime.NewScheme() + +func init() { + // internal + util.Must(internal.AddToScheme(cfgScheme)) + + // v1alpha1 + util.Must(v1alpha1.AddToScheme(cfgScheme)) + util.Must(cfgScheme.SetVersionPriority(v1alpha1.SchemeGroupVersion)) +} + +// ReadLeaderMigrationConfiguration reads LeaderMigrationConfiguration from a YAML file at the given path. +// The parsed LeaderMigrationConfiguration may be invalid. +// It returns an error if the file did not exist. +func ReadLeaderMigrationConfiguration(configFilePath string) (*internal.LeaderMigrationConfiguration, error) { + data, err := ioutil.ReadFile(configFilePath) + if err != nil { + return nil, fmt.Errorf("unable to read leader migration configuration from %q: %v", configFilePath, err) + } + config, gvk, err := serializer.NewCodecFactory(cfgScheme).UniversalDecoder().Decode(data, nil, nil) + if err != nil { + return nil, err + } + internalConfig, ok := config.(*internal.LeaderMigrationConfiguration) + if !ok { + return nil, fmt.Errorf("unexpected config type: %v", gvk) + } + return internalConfig, nil +} + +// ValidateLeaderMigrationConfiguration validates the LeaderMigrationConfiguration against common errors. +// It checks required names and whether resourceLock is either 'leases' or 'endpoints'. +// It will return nil if it does not find anything wrong. +func ValidateLeaderMigrationConfiguration(config *internal.LeaderMigrationConfiguration) (allErrs field.ErrorList) { + if config.LeaderName == "" { + allErrs = append(allErrs, field.Required(field.NewPath("leaderName"), + "leaderName must be set for LeaderMigrationConfiguration")) + } + if config.ResourceLock != ResourceLockLeases && config.ResourceLock != ResourceLockEndpoints { + allErrs = append(allErrs, field.Invalid(field.NewPath("resourceLock"), config.ResourceLock, + "resource Lock must be one of 'leases' or 'endpoints'")) + } + // validate controllerLeaders + fldPath := field.NewPath("controllerLeaders") + for i, controllerLeader := range config.ControllerLeaders { + path := fldPath.Index(i) + allErrs = append(allErrs, validateControllerLeaderConfiguration(path, &controllerLeader)...) + } + return +} + +func validateControllerLeaderConfiguration(path *field.Path, config *internal.ControllerLeaderConfiguration) (allErrs field.ErrorList) { + if config == nil { + return + } + if config.Component == "" { + allErrs = append(allErrs, field.Required(path.Child("component"), "component must be set")) + } + if config.Name == "" { + allErrs = append(allErrs, field.Required(path.Child("name"), "name must be set")) + } + return +} diff --git a/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/config_test.go b/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/config_test.go new file mode 100644 index 00000000000..076d9fdac29 --- /dev/null +++ b/staging/src/k8s.io/controller-manager/pkg/leadermigration/config/config_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2020 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 config + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + internal "k8s.io/controller-manager/config" +) + +func TestReadLeaderMigrationConfiguration(t *testing.T) { + testCases := []struct { + name string + content string + expected *internal.LeaderMigrationConfiguration + expectErr bool + }{ + { + name: "empty", + content: "", + expected: nil, + expectErr: true, + }, + { + name: "wrong type", + content: ` +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +`, + expected: nil, + expectErr: true, + }, + { + name: "basic", + content: ` +apiVersion: controllermanager.config.k8s.io/v1alpha1 +kind: LeaderMigrationConfiguration +leaderName: migration-120-to-121 +resourceLock: leases +controllerLeaders: [] +`, + expected: &internal.LeaderMigrationConfiguration{ + TypeMeta: metav1.TypeMeta{}, + LeaderName: "migration-120-to-121", + ResourceLock: "leases", + ControllerLeaders: []internal.ControllerLeaderConfiguration{}, + }, + expectErr: false, + }, + { + name: "endpoints", + content: ` +apiVersion: controllermanager.config.k8s.io/v1alpha1 +kind: LeaderMigrationConfiguration +leaderName: migration-120-to-121 +resourceLock: endpoints +controllerLeaders: [] +`, + expected: &internal.LeaderMigrationConfiguration{ + TypeMeta: metav1.TypeMeta{}, + LeaderName: "migration-120-to-121", + ResourceLock: "endpoints", + ControllerLeaders: []internal.ControllerLeaderConfiguration{}, + }, + }, + { + name: "withLeaders", + content: ` +apiVersion: controllermanager.config.k8s.io/v1alpha1 +kind: LeaderMigrationConfiguration +leaderName: migration-120-to-121 +resourceLock: endpoints +controllerLeaders: + - name: route-controller + component: kube-controller-manager + - name: service-controller + component: kube-controller-manager +`, + expected: &internal.LeaderMigrationConfiguration{ + TypeMeta: metav1.TypeMeta{}, + LeaderName: "migration-120-to-121", + ResourceLock: "endpoints", + ControllerLeaders: []internal.ControllerLeaderConfiguration{ + { + Name: "route-controller", + Component: "kube-controller-manager", + }, + { + Name: "service-controller", + Component: "kube-controller-manager", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + configFile, err := ioutil.TempFile("", tc.name) + if err != nil { + t.Fatal(err) + } + defer os.Remove(configFile.Name()) + err = ioutil.WriteFile(configFile.Name(), []byte(tc.content), os.FileMode(0755)) + if err != nil { + t.Fatal(err) + } + result, err := ReadLeaderMigrationConfiguration(configFile.Name()) + if tc.expectErr && err == nil { + t.Errorf("unexpected no error for %s", tc.name) + } else if !tc.expectErr && err != nil { + t.Errorf("get error from ReadLeaderElectionConfiguration: %#v", err) + } else if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("result not matching expected, got %#v, expected %#v", result, tc.expected) + } + }) + } +} + +func TestValidateLeaderMigrationConfiguration(t *testing.T) { + testCases := []struct { + name string + config *internal.LeaderMigrationConfiguration + expectErr bool + }{ + { + name: "empty name", + config: &internal.LeaderMigrationConfiguration{ + LeaderName: "", + ResourceLock: ResourceLockLeases, + ControllerLeaders: []internal.ControllerLeaderConfiguration{}, + }, + expectErr: true, + }, + { + name: "invalid resourceLock", + config: &internal.LeaderMigrationConfiguration{ + LeaderName: "test", + ResourceLock: "invalid", + ControllerLeaders: []internal.ControllerLeaderConfiguration{}, + }, + expectErr: true, + }, + { + name: "empty controllerLeaders (valid)", + config: &internal.LeaderMigrationConfiguration{ + LeaderName: "test", + ResourceLock: ResourceLockLeases, + ControllerLeaders: []internal.ControllerLeaderConfiguration{}, + }, + expectErr: false, + }, + { + name: "endpoints", + config: &internal.LeaderMigrationConfiguration{ + TypeMeta: metav1.TypeMeta{}, + LeaderName: "migration-120-to-121", + ResourceLock: ResourceLockEndpoints, + ControllerLeaders: []internal.ControllerLeaderConfiguration{ + { + Name: "route-controller", + Component: "kube-controller-manager", + }, + }, + }, + expectErr: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + errs := ValidateLeaderMigrationConfiguration(tc.config) + if tc.expectErr && len(errs) == 0 { + t.Errorf("calling ValidateLeaderMigrationConfiguration expected errors but got no error") + } + if !tc.expectErr && len(errs) != 0 { + t.Errorf("calling ValidateLeaderMigrationConfiguration expected no error but got %v", errs) + } + }) + } +}