From d476dc22b051cd9bc03636a46300d7900cacdebb Mon Sep 17 00:00:00 2001
From: Stoyan Rachev CloudControllerManager contains configuration settings for the cloud-controller-manager.
+ControlPlaneConfig
+
+
+cloudControllerManager
+
+
+CloudControllerManagerConfig
+
+
+
+(Optional)
+
+InfrastructureConfig
@@ -192,6 +206,37 @@ reconciliation is possible.
+(Appears on: +ControlPlaneConfig) +
++
CloudControllerManagerConfig contains configuration settings for the cloud-controller-manager.
+ +Field | +Description | +
---|---|
+featureGates
+
+map[string]bool
+
+ |
+
+(Optional)
+ FeatureGates contains information about enabled feature gates. + |
+
diff --git a/pkg/apis/kubevirt/types_controlplane.go b/pkg/apis/kubevirt/types_controlplane.go index 7442ff6e..96daa15f 100644 --- a/pkg/apis/kubevirt/types_controlplane.go +++ b/pkg/apis/kubevirt/types_controlplane.go @@ -23,4 +23,14 @@ import ( // ControlPlaneConfig contains configuration settings for the control plane. type ControlPlaneConfig struct { metav1.TypeMeta + + // CloudControllerManager contains configuration settings for the cloud-controller-manager. + // +optional + CloudControllerManager *CloudControllerManagerConfig +} + +// CloudControllerManagerConfig contains configuration settings for the cloud-controller-manager. +type CloudControllerManagerConfig struct { + // FeatureGates contains information about enabled feature gates. + FeatureGates map[string]bool } diff --git a/pkg/apis/kubevirt/v1alpha1/types_controlplane.go b/pkg/apis/kubevirt/v1alpha1/types_controlplane.go index b54de462..b4ba8956 100644 --- a/pkg/apis/kubevirt/v1alpha1/types_controlplane.go +++ b/pkg/apis/kubevirt/v1alpha1/types_controlplane.go @@ -24,4 +24,15 @@ import ( // ControlPlaneConfig contains configuration settings for the control plane. type ControlPlaneConfig struct { metav1.TypeMeta `json:",inline"` + + // CloudControllerManager contains configuration settings for the cloud-controller-manager. + // +optional + CloudControllerManager *CloudControllerManagerConfig `json:"cloudControllerManager,omitempty"` +} + +// CloudControllerManagerConfig contains configuration settings for the cloud-controller-manager. +type CloudControllerManagerConfig struct { + // FeatureGates contains information about enabled feature gates. + // +optional + FeatureGates map[string]bool `json:"featureGates,omitempty"` } diff --git a/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go b/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go index 6ac60021..6e1b80f9 100644 --- a/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go @@ -35,6 +35,16 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*CloudControllerManagerConfig)(nil), (*kubevirt.CloudControllerManagerConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_CloudControllerManagerConfig_To_kubevirt_CloudControllerManagerConfig(a.(*CloudControllerManagerConfig), b.(*kubevirt.CloudControllerManagerConfig), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*kubevirt.CloudControllerManagerConfig)(nil), (*CloudControllerManagerConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_kubevirt_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(a.(*kubevirt.CloudControllerManagerConfig), b.(*CloudControllerManagerConfig), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*CloudProfileConfig)(nil), (*kubevirt.CloudProfileConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_CloudProfileConfig_To_kubevirt_CloudProfileConfig(a.(*CloudProfileConfig), b.(*kubevirt.CloudProfileConfig), scope) }); err != nil { @@ -128,6 +138,26 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1alpha1_CloudControllerManagerConfig_To_kubevirt_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kubevirt.CloudControllerManagerConfig, s conversion.Scope) error { + out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) + return nil +} + +// Convert_v1alpha1_CloudControllerManagerConfig_To_kubevirt_CloudControllerManagerConfig is an autogenerated conversion function. +func Convert_v1alpha1_CloudControllerManagerConfig_To_kubevirt_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kubevirt.CloudControllerManagerConfig, s conversion.Scope) error { + return autoConvert_v1alpha1_CloudControllerManagerConfig_To_kubevirt_CloudControllerManagerConfig(in, out, s) +} + +func autoConvert_kubevirt_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in *kubevirt.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { + out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) + return nil +} + +// Convert_kubevirt_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig is an autogenerated conversion function. +func Convert_kubevirt_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in *kubevirt.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { + return autoConvert_kubevirt_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in, out, s) +} + func autoConvert_v1alpha1_CloudProfileConfig_To_kubevirt_CloudProfileConfig(in *CloudProfileConfig, out *kubevirt.CloudProfileConfig, s conversion.Scope) error { out.MachineImages = *(*[]kubevirt.MachineImages)(unsafe.Pointer(&in.MachineImages)) out.MachineDeploymentConfig = *(*[]kubevirt.MachineDeploymentConfig)(unsafe.Pointer(&in.MachineDeploymentConfig)) @@ -151,6 +181,7 @@ func Convert_kubevirt_CloudProfileConfig_To_v1alpha1_CloudProfileConfig(in *kube } func autoConvert_v1alpha1_ControlPlaneConfig_To_kubevirt_ControlPlaneConfig(in *ControlPlaneConfig, out *kubevirt.ControlPlaneConfig, s conversion.Scope) error { + out.CloudControllerManager = (*kubevirt.CloudControllerManagerConfig)(unsafe.Pointer(in.CloudControllerManager)) return nil } @@ -160,6 +191,7 @@ func Convert_v1alpha1_ControlPlaneConfig_To_kubevirt_ControlPlaneConfig(in *Cont } func autoConvert_kubevirt_ControlPlaneConfig_To_v1alpha1_ControlPlaneConfig(in *kubevirt.ControlPlaneConfig, out *ControlPlaneConfig, s conversion.Scope) error { + out.CloudControllerManager = (*CloudControllerManagerConfig)(unsafe.Pointer(in.CloudControllerManager)) return nil } diff --git a/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go index 3c500afc..2e63eb5d 100644 --- a/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,29 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudControllerManagerConfig) DeepCopyInto(out *CloudControllerManagerConfig) { + *out = *in + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudControllerManagerConfig. +func (in *CloudControllerManagerConfig) DeepCopy() *CloudControllerManagerConfig { + if in == nil { + return nil + } + out := new(CloudControllerManagerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudProfileConfig) DeepCopyInto(out *CloudProfileConfig) { *out = *in @@ -65,6 +88,11 @@ func (in *CloudProfileConfig) DeepCopyObject() runtime.Object { func (in *ControlPlaneConfig) DeepCopyInto(out *ControlPlaneConfig) { *out = *in out.TypeMeta = in.TypeMeta + if in.CloudControllerManager != nil { + in, out := &in.CloudControllerManager, &out.CloudControllerManager + *out = new(CloudControllerManagerConfig) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/kubevirt/zz_generated.deepcopy.go b/pkg/apis/kubevirt/zz_generated.deepcopy.go index 826c74ad..eb20f164 100644 --- a/pkg/apis/kubevirt/zz_generated.deepcopy.go +++ b/pkg/apis/kubevirt/zz_generated.deepcopy.go @@ -24,6 +24,29 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudControllerManagerConfig) DeepCopyInto(out *CloudControllerManagerConfig) { + *out = *in + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudControllerManagerConfig. +func (in *CloudControllerManagerConfig) DeepCopy() *CloudControllerManagerConfig { + if in == nil { + return nil + } + out := new(CloudControllerManagerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudProfileConfig) DeepCopyInto(out *CloudProfileConfig) { *out = *in @@ -65,6 +88,11 @@ func (in *CloudProfileConfig) DeepCopyObject() runtime.Object { func (in *ControlPlaneConfig) DeepCopyInto(out *ControlPlaneConfig) { *out = *in out.TypeMeta = in.TypeMeta + if in.CloudControllerManager != nil { + in, out := &in.CloudControllerManager, &out.CloudControllerManager + *out = new(CloudControllerManagerConfig) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index e2b350b4..61e7d492 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -76,11 +76,6 @@ func (c *Config) ApplyETCDStorage(etcdStorage *config.ETCDStorage) { *etcdStorage = c.Config.ETCD.Storage } -// ApplyGardenId sets the gardenId. -func (c *Config) ApplyGardenId(gardenId *string) { - *gardenId = c.Config.GardenId -} - // Options initializes empty config.ControllerConfiguration, applies the set values and returns it. func (c *Config) Options() config.ControllerConfiguration { var cfg config.ControllerConfiguration diff --git a/pkg/controller/controlplane/add.go b/pkg/controller/controlplane/add.go index 439c6926..3bddcdde 100644 --- a/pkg/controller/controlplane/add.go +++ b/pkg/controller/controlplane/add.go @@ -15,15 +15,13 @@ package controlplane import ( - "context" - + "github.com/gardener/gardener-extension-provider-kubevirt/pkg/imagevector" "github.com/gardener/gardener-extension-provider-kubevirt/pkg/kubevirt" extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" - "github.com/gardener/gardener/extensions/pkg/controller/common" "github.com/gardener/gardener/extensions/pkg/controller/controlplane" - extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" - "github.com/go-logr/logr" + "github.com/gardener/gardener/extensions/pkg/controller/controlplane/genericactuator" + "github.com/gardener/gardener/extensions/pkg/util" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -32,23 +30,25 @@ import ( var ( // DefaultAddOptions are the default AddOptions for AddToManager. DefaultAddOptions = AddOptions{} + + logger = log.Log.WithName("kubevirt-controlplane-controller") ) -// AddOptions are options to apply when adding the KubeVirt controlplane controller to the manager. +// AddOptions are options to apply when adding the Kubevirt controlplane controller to the manager. type AddOptions struct { // Controller are the controller.Options. Controller controller.Options // IgnoreOperationAnnotation specifies whether to ignore the operation annotation or not. IgnoreOperationAnnotation bool - // GardenId is the Gardener garden identity - GardenId string } // AddToManagerWithOptions adds a controller with the given Options to the given manager. // The opts.Reconciler is being set with a newly instantiated actuator. func AddToManagerWithOptions(mgr manager.Manager, opts AddOptions) error { return controlplane.Add(mgr, controlplane.AddArgs{ - Actuator: NewActuator(opts.GardenId), + Actuator: genericactuator.NewActuator(kubevirt.Name, controlPlaneSecrets, nil, configChart, controlPlaneChart, controlPlaneShootChart, + storageClassChart, nil, NewValuesProvider(logger), extensionscontroller.ChartRendererFactoryFunc(util.NewChartRendererForShoot), + imagevector.ImageVector(), "", nil, mgr.GetWebhookServer().Port, logger), ControllerOptions: opts.Controller, Predicates: controlplane.DefaultPredicates(opts.IgnoreOperationAnnotation), Type: kubevirt.Type, @@ -59,39 +59,3 @@ func AddToManagerWithOptions(mgr manager.Manager, opts AddOptions) error { func AddToManager(mgr manager.Manager) error { return AddToManagerWithOptions(mgr, DefaultAddOptions) } - -// NewActuator creates a new Actuator that updates the status of the handled Infrastructure resources. -func NewActuator(gardenID string) controlplane.Actuator { - return &actuator{ - logger: log.Log.WithName("infrastructure-actuator"), - gardenID: gardenID, - } -} - -type actuator struct { - common.ChartRendererContext - - logger logr.Logger - gardenID string -} - -func (a *actuator) Reconcile(context.Context, *extensionsv1alpha1.ControlPlane, *extensionscontroller.Cluster) (bool, error) { - a.logger.Info("control-plane reconciled") - // TODO: install kubevirt-cloud-controller-manager here and related components, the genericActuator might be used here - return false, nil -} - -// Delete deletes the ControlPlane. -func (a *actuator) Delete(context.Context, *extensionsv1alpha1.ControlPlane, *extensionscontroller.Cluster) error { - return nil -} - -// Restore restores the ControlPlane. -func (a *actuator) Restore(context.Context, *extensionsv1alpha1.ControlPlane, *extensionscontroller.Cluster) (bool, error) { - return false, nil -} - -// Migrate migrates the ControlPlane. -func (a *actuator) Migrate(context.Context, *extensionsv1alpha1.ControlPlane, *extensionscontroller.Cluster) error { - return nil -} diff --git a/pkg/controller/controlplane/controlplane_suite_test.go b/pkg/controller/controlplane/controlplane_suite_test.go new file mode 100644 index 00000000..3dfd8023 --- /dev/null +++ b/pkg/controller/controlplane/controlplane_suite_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 controlplane_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestControlplane(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controlplane Suite") +} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go new file mode 100644 index 00000000..928919dc --- /dev/null +++ b/pkg/controller/controlplane/valuesprovider.go @@ -0,0 +1,259 @@ +// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 controlplane + +import ( + "context" + "path/filepath" + + apiskubevirt "github.com/gardener/gardener-extension-provider-kubevirt/pkg/apis/kubevirt" + "github.com/gardener/gardener-extension-provider-kubevirt/pkg/kubevirt" + + extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" + "github.com/gardener/gardener/extensions/pkg/controller/controlplane" + "github.com/gardener/gardener/extensions/pkg/controller/controlplane/genericactuator" + "github.com/gardener/gardener/extensions/pkg/util" + v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + "github.com/gardener/gardener/pkg/utils/chart" + "github.com/gardener/gardener/pkg/utils/secrets" + "github.com/go-logr/logr" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apiserver/pkg/authentication/user" + autoscalingv1beta2 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta2" +) + +var ( + controlPlaneSecrets = &secrets.Secrets{ + CertificateSecretConfigs: map[string]*secrets.CertificateSecretConfig{ + v1beta1constants.SecretNameCACluster: { + Name: v1beta1constants.SecretNameCACluster, + CommonName: "kubernetes", + CertType: secrets.CACert, + }, + }, + SecretConfigsFunc: func(cas map[string]*secrets.Certificate, clusterName string) []secrets.ConfigInterface { + return []secrets.ConfigInterface{ + &secrets.ControlPlaneSecretConfig{ + CertificateSecretConfig: &secrets.CertificateSecretConfig{ + Name: kubevirt.CloudControllerManagerName, + CommonName: "system:cloud-controller-manager", + Organization: []string{user.SystemPrivilegedGroup}, + CertType: secrets.ClientCert, + SigningCA: cas[v1beta1constants.SecretNameCACluster], + }, + KubeConfigRequest: &secrets.KubeConfigRequest{ + ClusterName: clusterName, + APIServerURL: v1beta1constants.DeploymentNameKubeAPIServer, + }, + }, + &secrets.ControlPlaneSecretConfig{ + CertificateSecretConfig: &secrets.CertificateSecretConfig{ + Name: kubevirt.CloudControllerManagerName + "-server", + CommonName: kubevirt.CloudControllerManagerName, + DNSNames: controlplane.DNSNamesForService(kubevirt.CloudControllerManagerName, clusterName), + CertType: secrets.ServerCert, + SigningCA: cas[v1beta1constants.SecretNameCACluster], + }, + }, + } + }, + } + + configChart = &chart.Chart{ + Name: "cloud-provider-config", + Path: filepath.Join(kubevirt.InternalChartsPath, "cloud-provider-config"), + Objects: []*chart.Object{ + { + Type: &corev1.Secret{}, + Name: kubevirt.CloudProviderConfigName, + }, + }, + } + + controlPlaneChart = &chart.Chart{ + Name: "seed-controlplane", + Path: filepath.Join(kubevirt.InternalChartsPath, "seed-controlplane"), + SubCharts: []*chart.Chart{ + { + Name: kubevirt.CloudControllerManagerName, + Images: []string{kubevirt.CloudControllerManagerImageName}, + Objects: []*chart.Object{ + {Type: &corev1.Service{}, Name: kubevirt.CloudControllerManagerName}, + {Type: &appsv1.Deployment{}, Name: kubevirt.CloudControllerManagerName}, + {Type: &corev1.ConfigMap{}, Name: kubevirt.CloudControllerManagerName + "-monitoring-config"}, + {Type: &autoscalingv1beta2.VerticalPodAutoscaler{}, Name: kubevirt.CloudControllerManagerName + "-vpa"}, + }, + }, + }, + } + + controlPlaneShootChart = &chart.Chart{ + Name: "shoot-system-components", + Path: filepath.Join(kubevirt.InternalChartsPath, "shoot-system-components"), + SubCharts: []*chart.Chart{ + { + Name: kubevirt.CloudControllerManagerName, + Objects: []*chart.Object{ + {Type: &rbacv1.ClusterRole{}, Name: "system:cloud-controller-manager"}, + {Type: &rbacv1.ClusterRoleBinding{}, Name: "system:cloud-controller-manager"}, + {Type: &rbacv1.ClusterRole{}, Name: "system:controller:cloud-node-controller"}, + {Type: &rbacv1.ClusterRoleBinding{}, Name: "system:controller:cloud-node-controller"}, + }, + }, + }, + } + + storageClassChart = &chart.Chart{ + Name: "shoot-storageclasses", + Path: filepath.Join(kubevirt.InternalChartsPath, "shoot-storageclasses"), + } +) + +// NewValuesProvider creates a new ValuesProvider for the generic actuator. +func NewValuesProvider(logger logr.Logger) genericactuator.ValuesProvider { + return &valuesProvider{ + logger: logger.WithName("kubevirt-values-provider"), + } +} + +// valuesProvider is a ValuesProvider that provides kubevirt-specific values for the 2 charts applied by the generic actuator. +type valuesProvider struct { + genericactuator.NoopValuesProvider + logger logr.Logger +} + +// GetConfigChartValues returns the values for the config chart applied by the generic actuator. +func (vp *valuesProvider) GetConfigChartValues( + ctx context.Context, + cp *extensionsv1alpha1.ControlPlane, + _ *extensionscontroller.Cluster, +) (map[string]interface{}, error) { + // Get kubeconfig + kubeconfig, err := kubevirt.GetKubeConfig(ctx, vp.Client(), cp.Spec.SecretRef) + if err != nil { + return nil, errors.Wrapf(err, "could not get kubeconfig from secret '%s/%s'", cp.Spec.SecretRef.Namespace, cp.Spec.SecretRef.Name) + } + + // Get config chart values + return getConfigChartValues(kubeconfig) +} + +// GetControlPlaneChartValues returns the values for the control plane chart applied by the generic actuator. +func (vp *valuesProvider) GetControlPlaneChartValues( + _ context.Context, + cp *extensionsv1alpha1.ControlPlane, + cluster *extensionscontroller.Cluster, + checksums map[string]string, + scaledDown bool, +) (map[string]interface{}, error) { + // Decode providerConfig + cpConfig := &apiskubevirt.ControlPlaneConfig{} + if cp.Spec.ProviderConfig != nil { + if _, _, err := vp.Decoder().Decode(cp.Spec.ProviderConfig.Raw, nil, cpConfig); err != nil { + return nil, errors.Wrapf(err, "could not decode providerConfig of controlplane '%s'", util.ObjectName(cp)) + } + } + + return getControlPlaneChartValues(cpConfig, cp, cluster, checksums, scaledDown) +} + +// GetControlPlaneShootChartValues returns the values for the control plane shoot chart applied by the generic actuator. +func (vp *valuesProvider) GetControlPlaneShootChartValues( + _ context.Context, + _ *extensionsv1alpha1.ControlPlane, + _ *extensionscontroller.Cluster, + _ map[string]string, +) (map[string]interface{}, error) { + return getControlPlaneShootChartValues(), nil +} + +// GetStorageClassesChartValues returns the values for the storage classes chart applied by the generic actuator. +func (vp *valuesProvider) GetStorageClassesChartValues( + _ context.Context, + _ *extensionsv1alpha1.ControlPlane, + _ *extensionscontroller.Cluster, +) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + +// getConfigChartValues collects and returns the configuration chart values. +func getConfigChartValues(kubeconfig []byte) (map[string]interface{}, error) { + // Collect config chart values. + values := map[string]interface{}{ + "kubeconfig": string(kubeconfig), + } + + return values, nil +} + +// getControlPlaneChartValues collects and returns the control plane chart values. +func getControlPlaneChartValues( + cpConfig *apiskubevirt.ControlPlaneConfig, + cp *extensionsv1alpha1.ControlPlane, + cluster *extensionscontroller.Cluster, + checksums map[string]string, + scaledDown bool, +) (map[string]interface{}, error) { + ccm, err := getCCMChartValues(cpConfig, cp, cluster, checksums, scaledDown) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + kubevirt.CloudControllerManagerName: ccm, + }, nil +} + +// getCCMChartValues collects and returns the CCM chart values. +func getCCMChartValues( + cpConfig *apiskubevirt.ControlPlaneConfig, + cp *extensionsv1alpha1.ControlPlane, + cluster *extensionscontroller.Cluster, + checksums map[string]string, + scaledDown bool, +) (map[string]interface{}, error) { + values := map[string]interface{}{ + "enabled": true, + "replicas": extensionscontroller.GetControlPlaneReplicas(cluster, scaledDown, 1), + "clusterName": cp.Namespace, + "kubernetesVersion": cluster.Shoot.Spec.Kubernetes.Version, + "podNetwork": extensionscontroller.GetPodNetwork(cluster), + "podAnnotations": map[string]interface{}{ + "checksum/secret-" + kubevirt.CloudControllerManagerName: checksums[kubevirt.CloudControllerManagerName], + "checksum/secret-" + kubevirt.CloudControllerManagerName + "-server": checksums[kubevirt.CloudControllerManagerName+"-server"], + "checksum/secret-" + kubevirt.CloudProviderConfigName: checksums[kubevirt.CloudProviderConfigName], + }, + "podLabels": map[string]interface{}{ + v1beta1constants.LabelPodMaintenanceRestart: "true", + }, + } + + if cpConfig.CloudControllerManager != nil { + values["featureGates"] = cpConfig.CloudControllerManager.FeatureGates + } + + return values, nil +} + +// getControlPlaneShootChartValues collects and returns the control plane shoot chart values. +func getControlPlaneShootChartValues() map[string]interface{} { + return map[string]interface{}{ + kubevirt.CloudControllerManagerName: map[string]interface{}{"enabled": true}, + } +} diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go new file mode 100644 index 00000000..bf2b89f8 --- /dev/null +++ b/pkg/controller/controlplane/valuesprovider_test.go @@ -0,0 +1,214 @@ +// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 controlplane + +import ( + "context" + "encoding/json" + + apiskubevirt "github.com/gardener/gardener-extension-provider-kubevirt/pkg/apis/kubevirt" + "github.com/gardener/gardener-extension-provider-kubevirt/pkg/kubevirt" + + extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" + "github.com/gardener/gardener/extensions/pkg/controller/controlplane/genericactuator" + gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" + v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + mockclient "github.com/gardener/gardener/pkg/mock/controller-runtime/client" + "github.com/gardener/gardener/pkg/utils" + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/runtime/inject" +) + +const ( + namespace = "test" +) + +var _ = Describe("ValuesProvider", func() { + var ( + ctrl *gomock.Controller + ctx = context.TODO() + logger = log.Log.WithName("test") + + c *mockclient.MockClient + vp genericactuator.ValuesProvider + + scheme = runtime.NewScheme() + _ = apiskubevirt.AddToScheme(scheme) + + infrastructureStatus = &apiskubevirt.InfrastructureStatus{} + + cp = &extensionsv1alpha1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "control-plane", + Namespace: namespace, + }, + Spec: extensionsv1alpha1.ControlPlaneSpec{ + Region: "local", + SecretRef: corev1.SecretReference{ + Name: v1beta1constants.SecretNameCloudProvider, + Namespace: namespace, + }, + DefaultSpec: extensionsv1alpha1.DefaultSpec{ + ProviderConfig: &runtime.RawExtension{ + Raw: encode(&apiskubevirt.ControlPlaneConfig{ + CloudControllerManager: &apiskubevirt.CloudControllerManagerConfig{ + FeatureGates: map[string]bool{ + "CustomResourceValidation": true, + }, + }, + }), + }, + }, + InfrastructureProviderStatus: &runtime.RawExtension{ + Raw: encode(infrastructureStatus), + }, + }, + } + + cidr = "10.250.0.0/19" + cluster = &extensionscontroller.Cluster{ + Shoot: &gardencorev1beta1.Shoot{ + Spec: gardencorev1beta1.ShootSpec{ + Networking: gardencorev1beta1.Networking{ + Pods: &cidr, + }, + Kubernetes: gardencorev1beta1.Kubernetes{ + Version: "1.13.4", + }, + }, + }, + } + + cpSecretKey = client.ObjectKey{Namespace: namespace, Name: v1beta1constants.SecretNameCloudProvider} + cpSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: v1beta1constants.SecretNameCloudProvider, + Namespace: namespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "kubeconfig": []byte(`kubeconfig`), + }, + } + + checksums = map[string]string{ + kubevirt.CloudControllerManagerName: "3d791b164a808638da9a8df03924be2a41e34cd664e42231c00fe369e3588272", + kubevirt.CloudControllerManagerName + "-server": "6dff2a2e6f14444b66d8e4a351c049f7e89ee24ba3eaab95dbec40ba6bdebb52", + kubevirt.CloudProviderConfigName: "8bafb35ff1ac60275d62e1cbd495aceb511fb354f74a20f7d06ecb48b3a68432", + } + + enabledTrue = map[string]interface{}{"enabled": true} + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + + c = mockclient.NewMockClient(ctrl) + vp = NewValuesProvider(logger) + + err := vp.(inject.Scheme).InjectScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = vp.(inject.Client).InjectClient(c) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + ctrl.Finish() + }) + + Describe("#GetConfigChartValues", func() { + It("should return correct config chart values", func() { + c.EXPECT().Get(ctx, cpSecretKey, &corev1.Secret{}).DoAndReturn(clientGet(cpSecret)) + + values, err := vp.GetConfigChartValues(ctx, cp, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal(map[string]interface{}{ + "kubeconfig": "kubeconfig", + })) + }) + }) + + Describe("#GetControlPlaneChartValues", func() { + ccmChartValues := utils.MergeMaps(enabledTrue, map[string]interface{}{ + "replicas": 1, + "clusterName": namespace, + "kubernetesVersion": "1.17.5", + "podNetwork": cidr, + "podAnnotations": map[string]interface{}{ + "checksum/secret-cloud-controller-manager": "3d791b164a808638da9a8df03924be2a41e34cd664e42231c00fe369e3588272", + "checksum/secret-cloud-controller-manager-server": "6dff2a2e6f14444b66d8e4a351c049f7e89ee24ba3eaab95dbec40ba6bdebb52", + "checksum/secret-cloud-provider-config": "8bafb35ff1ac60275d62e1cbd495aceb511fb354f74a20f7d06ecb48b3a68432", + }, + "podLabels": map[string]interface{}{ + "maintenance.gardener.cloud/restart": "true", + }, + "featureGates": map[string]bool{ + "CustomResourceValidation": true, + }, + }) + + It("should return correct control plane chart values", func() { + values, err := vp.GetControlPlaneChartValues(ctx, cp, cluster, checksums, false) + + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal(map[string]interface{}{ + kubevirt.CloudControllerManagerName: utils.MergeMaps(ccmChartValues, map[string]interface{}{ + "kubernetesVersion": cluster.Shoot.Spec.Kubernetes.Version, + }), + })) + }) + }) + + Describe("#GetControlPlaneShootChartValues", func() { + It("should return correct control plane shoot chart values", func() { + values, err := vp.GetControlPlaneShootChartValues(ctx, cp, cluster, checksums) + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal(map[string]interface{}{ + kubevirt.CloudControllerManagerName: enabledTrue, + })) + }) + }) + + Describe("#GetStorageClassesChartValues()", func() { + It("should return empty storage class chart values", func() { + values, err := vp.GetStorageClassesChartValues(ctx, cp, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal(map[string]interface{}{})) + }) + }) +}) + +func encode(obj runtime.Object) []byte { + data, _ := json.Marshal(obj) + return data +} + +func clientGet(result runtime.Object) interface{} { + return func(ctx context.Context, key client.ObjectKey, obj runtime.Object) error { + switch obj.(type) { + case *corev1.Secret: + *obj.(*corev1.Secret) = *result.(*corev1.Secret) + } + return nil + } +} diff --git a/pkg/controller/infrastructure/actuator.go b/pkg/controller/infrastructure/actuator.go index f70b9f1a..2814babe 100644 --- a/pkg/controller/infrastructure/actuator.go +++ b/pkg/controller/infrastructure/actuator.go @@ -24,15 +24,13 @@ import ( type actuator struct { common.ChartRendererContext - logger logr.Logger - gardenID string + logger logr.Logger } // NewActuator creates a new Actuator that updates the status of the handled Infrastructure resources. -func NewActuator(gardenID string) infrastructure.Actuator { +func NewActuator() infrastructure.Actuator { return &actuator{ - logger: log.Log.WithName("infrastructure-actuator"), - gardenID: gardenID, + logger: log.Log.WithName("infrastructure-actuator"), } } diff --git a/pkg/controller/infrastructure/add.go b/pkg/controller/infrastructure/add.go index a2f21712..bdc5196a 100644 --- a/pkg/controller/infrastructure/add.go +++ b/pkg/controller/infrastructure/add.go @@ -33,15 +33,13 @@ type AddOptions struct { Controller controller.Options // IgnoreOperationAnnotation specifies whether to ignore the operation annotation or not. IgnoreOperationAnnotation bool - // GardenId is the Gardener garden identity - GardenId string } // AddToManagerWithOptions adds a controller with the given Options to the given manager. // The opts.Reconciler is being set with a newly instantiated actuator. func AddToManagerWithOptions(mgr manager.Manager, opts AddOptions) error { return infrastructure.Add(mgr, infrastructure.AddArgs{ - Actuator: NewActuator(opts.GardenId), + Actuator: NewActuator(), ControllerOptions: opts.Controller, Predicates: infrastructure.DefaultPredicates(opts.IgnoreOperationAnnotation), Type: kubevirt.Type, diff --git a/pkg/kubevirt/types.go b/pkg/kubevirt/types.go index 97bcb76e..9fdaf93a 100644 --- a/pkg/kubevirt/types.go +++ b/pkg/kubevirt/types.go @@ -21,6 +21,14 @@ import ( const ( // Name is the name of the KubeVirt provider controller. Name = "provider-kubevirt" + + // CloudControllerManagerImageName is the name of the cloud-controller-manager image. + CloudControllerManagerImageName = "cloud-controller-manager" + + // CloudProviderConfigName is the name of the secret containing the cloud provider config. + CloudProviderConfigName = "cloud-provider-config" + // CloudControllerManagerName is a constant for the name of the cloud-controller-manager. + CloudControllerManagerName = "cloud-controller-manager" // MachineControllerManagerName is a constant for the name of the machine-controller-manager. MachineControllerManagerName = "machine-controller-manager" // MachineControllerManagerImageName is the name of the MachineControllerManager image. diff --git a/pkg/kubevirt/util.go b/pkg/kubevirt/util.go new file mode 100644 index 00000000..4f690a43 --- /dev/null +++ b/pkg/kubevirt/util.go @@ -0,0 +1,33 @@ +// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 kubevirt + +import ( + "context" + + extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// GetKubeConfig retrieves the kubeconfig specified by the secret reference. +func GetKubeConfig(ctx context.Context, c client.Client, secretRef corev1.SecretReference) ([]byte, error) { + secret, err := extensionscontroller.GetSecretByReference(ctx, c, &secretRef) + if err != nil { + return []byte(""), err + } + + return secret.Data["kubeconfig"], nil +} diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index bb27e613..baa3d22a 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -15,8 +15,17 @@ package controlplane import ( + "context" + "net" + + "github.com/coreos/go-systemd/unit" + extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook" "github.com/gardener/gardener/extensions/pkg/webhook/controlplane/genericmutator" "github.com/go-logr/logr" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -39,4 +48,96 @@ func (e *ensurer) InjectClient(client client.Client) error { return nil } -// TODO: modify control-plane arguments here, like api-server args etc. +// EnsureKubeAPIServerDeployment ensures that the kube-apiserver deployment conforms to the provider requirements. +func (e *ensurer) EnsureKubeAPIServerDeployment(ctx context.Context, ectx genericmutator.EnsurerContext, new, _ *appsv1.Deployment) error { + template := &new.Spec.Template + ps := &template.Spec + if c := extensionswebhook.ContainerWithName(ps.Containers, "kube-apiserver"); c != nil { + ensureKubeAPIServerCommandLineArgs(c) + } + + cluster, err := ectx.GetCluster(ctx) + if err != nil { + return err + } + + if cluster.Shoot != nil && cluster.Seed != nil && cluster.Shoot.Spec.Networking.Nodes != nil { + shootNodesIP, _, err := net.ParseCIDR(*cluster.Shoot.Spec.Networking.Nodes) + if err != nil { + return errors.Wrapf(err, "could not parse shoot nodes CIDR %s", *cluster.Shoot.Spec.Networking.Nodes) + } + _, seedPodsIPNet, err := net.ParseCIDR(cluster.Seed.Spec.Networks.Pods) + if err != nil { + return errors.Wrapf(err, "could not parse seed pods CIDR %s", cluster.Seed.Spec.Networks.Pods) + } + + // If the seed pods CIDR contains the shoot nodes CIDR (the seed cluster hosts the shoot cluster), + // delete the vpn-seed pod since VPN is not needed in this case, and if established it causes connections + // from pods in the seed cluster to the kube-apiserver pod to fail. + if seedPodsIPNet.Contains(shootNodesIP) { + ps.Containers = extensionswebhook.EnsureNoContainerWithName(ps.Containers, "vpn-seed") + } + } + + return nil +} + +// EnsureKubeControllerManagerDeployment ensures that the kube-controller-manager deployment conforms to the provider requirements. +func (e *ensurer) EnsureKubeControllerManagerDeployment(ctx context.Context, _ genericmutator.EnsurerContext, new, _ *appsv1.Deployment) error { + template := &new.Spec.Template + ps := &template.Spec + if c := extensionswebhook.ContainerWithName(ps.Containers, "kube-controller-manager"); c != nil { + ensureKubeControllerManagerCommandLineArgs(c) + } + return nil +} + +func ensureKubeAPIServerCommandLineArgs(c *corev1.Container) { + c.Command = extensionswebhook.EnsureNoStringWithPrefix(c.Command, "--cloud-provider=") + + // Ensure CSI-related admission plugins + c.Command = extensionswebhook.EnsureNoStringWithPrefixContains(c.Command, "--enable-admission-plugins=", + "PersistentVolumeLabel", ",") + c.Command = extensionswebhook.EnsureStringWithPrefixContains(c.Command, "--disable-admission-plugins=", + "PersistentVolumeLabel", ",") + + // Ensure CSI-related feature gates + c.Command = extensionswebhook.EnsureNoStringWithPrefixContains(c.Command, "--feature-gates=", + "CSINodeInfo=false", ",") + c.Command = extensionswebhook.EnsureNoStringWithPrefixContains(c.Command, "--feature-gates=", + "CSIDriverRegistry=false", ",") +} + +func ensureKubeControllerManagerCommandLineArgs(c *corev1.Container) { + c.Command = extensionswebhook.EnsureStringWithPrefix(c.Command, "--cloud-provider=", "external") +} + +// EnsureKubeletServiceUnitOptions ensures that the kubelet.service unit options conform to the provider requirements. +func (e *ensurer) EnsureKubeletServiceUnitOptions(ctx context.Context, _ genericmutator.EnsurerContext, new, _ []*unit.UnitOption) ([]*unit.UnitOption, error) { + if opt := extensionswebhook.UnitOptionWithSectionAndName(new, "Service", "ExecStart"); opt != nil { + command := extensionswebhook.DeserializeCommandLine(opt.Value) + command = ensureKubeletCommandLineArgs(command) + opt.Value = extensionswebhook.SerializeCommandLine(command, 1, " \\\n ") + } + + new = extensionswebhook.EnsureUnitOption(new, &unit.UnitOption{ + Section: "Service", + Name: "ExecStartPre", + Value: `/bin/sh -c 'hostnamectl set-hostname $(cat /etc/hostname | cut -d '.' -f 1)'`, + }) + return new, nil +} + +func ensureKubeletCommandLineArgs(command []string) []string { + command = extensionswebhook.EnsureStringWithPrefix(command, "--cloud-provider=", "external") + return command +} + +// EnsureKubeletConfiguration ensures that the kubelet configuration conforms to the provider requirements. +func (e *ensurer) EnsureKubeletConfiguration(ctx context.Context, _ genericmutator.EnsurerContext, new, _ *kubeletconfigv1beta1.KubeletConfiguration) error { + // Ensure CSI-related feature gates + delete(new.FeatureGates, "VolumeSnapshotDataSource") + delete(new.FeatureGates, "CSINodeInfo") + delete(new.FeatureGates, "CSIDriverRegistry") + return nil +} diff --git a/pkg/webhook/controlplane/ensurer_test.go b/pkg/webhook/controlplane/ensurer_test.go new file mode 100644 index 00000000..4efd5f14 --- /dev/null +++ b/pkg/webhook/controlplane/ensurer_test.go @@ -0,0 +1,354 @@ +// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 controlplane + +import ( + "context" + "testing" + + "github.com/coreos/go-systemd/unit" + extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" + extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook" + "github.com/gardener/gardener/extensions/pkg/webhook/controlplane/genericmutator" + "github.com/gardener/gardener/extensions/pkg/webhook/controlplane/test" + gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" + v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + mockclient "github.com/gardener/gardener/pkg/mock/controller-runtime/client" + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/runtime/inject" +) + +const ( + namespace = "test" +) + +func TestController(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controlplane Webhook Suite") +} + +var _ = Describe("Ensurer", func() { + var ( + ctrl *gomock.Controller + + emptyContext = genericmutator.NewInternalEnsurerContext(&extensionscontroller.Cluster{}) + seedHostsShootContext = genericmutator.NewInternalEnsurerContext( + &extensionscontroller.Cluster{ + Shoot: &gardencorev1beta1.Shoot{ + Spec: gardencorev1beta1.ShootSpec{ + Networking: gardencorev1beta1.Networking{ + Nodes: pointer.StringPtr("10.225.128.0/17"), + }, + }, + }, + Seed: &gardencorev1beta1.Seed{ + Spec: gardencorev1beta1.SeedSpec{ + Networks: gardencorev1beta1.SeedNetworks{ + Pods: "10.225.0.0/16", + }, + }, + }, + }, + ) + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + }) + + AfterEach(func() { + ctrl.Finish() + }) + + Describe("#EnsureKubeAPIServerDeployment", func() { + It("should add missing elements to kube-apiserver deployment", func() { + var ( + dep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: v1beta1constants.DeploymentNameKubeAPIServer}, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kube-apiserver", + }, + { + Name: "vpn-seed", + }, + }, + }, + }, + }, + } + ) + + // Create mock client + client := mockclient.NewMockClient(ctrl) + + // Create ensurer + ensurer := NewEnsurer(logger) + err := ensurer.(inject.Client).InjectClient(client) + Expect(err).To(Not(HaveOccurred())) + + // Call EnsureKubeAPIServerDeployment method and check the result + err = ensurer.EnsureKubeAPIServerDeployment(context.TODO(), emptyContext, dep, nil) + Expect(err).To(Not(HaveOccurred())) + checkKubeAPIServerDeployment(dep, true) + }) + + It("should modify existing elements of kube-apiserver deployment", func() { + var ( + dep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: v1beta1constants.DeploymentNameKubeAPIServer}, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kube-apiserver", + Command: []string{ + "--cloud-provider=?", + "--enable-admission-plugins=Priority,NamespaceLifecycle,PersistentVolumeLabel", + }, + }, + { + Name: "vpn-seed", + }, + }, + }, + }, + }, + } + ) + + // Create mock client + client := mockclient.NewMockClient(ctrl) + + // Create ensurer + ensurer := NewEnsurer(logger) + err := ensurer.(inject.Client).InjectClient(client) + Expect(err).To(Not(HaveOccurred())) + + // Call EnsureKubeAPIServerDeployment method and check the result + err = ensurer.EnsureKubeAPIServerDeployment(context.TODO(), emptyContext, dep, nil) + Expect(err).To(Not(HaveOccurred())) + checkKubeAPIServerDeployment(dep, true) + }) + + It("should delete the vpn-seed container if the seed hosts the shoot", func() { + var ( + dep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: v1beta1constants.DeploymentNameKubeAPIServer}, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kube-apiserver", + }, + { + Name: "vpn-seed", + }, + }, + }, + }, + }, + } + ) + + // Create mock client + client := mockclient.NewMockClient(ctrl) + + // Create ensurer + ensurer := NewEnsurer(logger) + err := ensurer.(inject.Client).InjectClient(client) + Expect(err).To(Not(HaveOccurred())) + + // Call EnsureKubeAPIServerDeployment method and check the result + err = ensurer.EnsureKubeAPIServerDeployment(context.TODO(), seedHostsShootContext, dep, nil) + Expect(err).To(Not(HaveOccurred())) + checkKubeAPIServerDeployment(dep, false) + }) + }) + + Describe("#EnsureKubeControllerManagerDeployment", func() { + It("should add missing elements to kube-controller-manager deployment", func() { + var ( + dep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: v1beta1constants.DeploymentNameKubeControllerManager}, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kube-controller-manager", + }, + }, + }, + }, + }, + } + ) + + // Create mock client + client := mockclient.NewMockClient(ctrl) + + // Create ensurer + ensurer := NewEnsurer(logger) + err := ensurer.(inject.Client).InjectClient(client) + Expect(err).To(Not(HaveOccurred())) + + // Call EnsureKubeControllerManagerDeployment method and check the result + err = ensurer.EnsureKubeControllerManagerDeployment(context.TODO(), emptyContext, dep, nil) + Expect(err).To(Not(HaveOccurred())) + checkKubeControllerManagerDeployment(dep) + }) + + It("should modify existing elements of kube-controller-manager deployment", func() { + var ( + dep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: v1beta1constants.DeploymentNameKubeControllerManager}, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + v1beta1constants.LabelNetworkPolicyToBlockedCIDRs: v1beta1constants.LabelNetworkPolicyAllowed, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kube-controller-manager", + Command: []string{ + "--cloud-provider=?", + }, + }, + }, + }, + }, + }, + } + ) + + // Create mock client + client := mockclient.NewMockClient(ctrl) + + // Create ensurer + ensurer := NewEnsurer(logger) + err := ensurer.(inject.Client).InjectClient(client) + Expect(err).To(Not(HaveOccurred())) + + // Call EnsureKubeControllerManagerDeployment method and check the result + err = ensurer.EnsureKubeControllerManagerDeployment(context.TODO(), emptyContext, dep, nil) + Expect(err).To(Not(HaveOccurred())) + checkKubeControllerManagerDeployment(dep) + }) + }) + + Describe("#EnsureKubeletServiceUnitOptions", func() { + It("should modify existing elements of kubelet.service unit options", func() { + var ( + oldUnitOptions = []*unit.UnitOption{ + { + Section: "Service", + Name: "ExecStart", + Value: `/opt/bin/hyperkube kubelet \ + --config=/var/lib/kubelet/config/kubelet`, + }, + } + newUnitOptions = []*unit.UnitOption{ + { + Section: "Service", + Name: "ExecStart", + Value: `/opt/bin/hyperkube kubelet \ + --config=/var/lib/kubelet/config/kubelet \ + --cloud-provider=external`, + }, + { + Section: "Service", + Name: "ExecStartPre", + Value: `/bin/sh -c 'hostnamectl set-hostname $(cat /etc/hostname | cut -d '.' -f 1)'`, + }, + } + ) + + // Create ensurer + ensurer := NewEnsurer(logger) + + // Call EnsureKubeletServiceUnitOptions method and check the result + opts, err := ensurer.EnsureKubeletServiceUnitOptions(context.TODO(), emptyContext, oldUnitOptions, nil) + Expect(err).To(Not(HaveOccurred())) + Expect(opts).To(Equal(newUnitOptions)) + }) + }) + + Describe("#EnsureKubeletConfiguration", func() { + It("should modify existing elements of kubelet configuration", func() { + var ( + oldKubeletConfig = &kubeletconfigv1beta1.KubeletConfiguration{ + FeatureGates: map[string]bool{ + "Foo": true, + "VolumeSnapshotDataSource": true, + "CSINodeInfo": true, + }, + } + newKubeletConfig = &kubeletconfigv1beta1.KubeletConfiguration{ + FeatureGates: map[string]bool{ + "Foo": true, + }, + } + ) + + // Create ensurer + ensurer := NewEnsurer(logger) + + // Call EnsureKubeletConfiguration method and check the result + kubeletConfig := *oldKubeletConfig + err := ensurer.EnsureKubeletConfiguration(context.TODO(), emptyContext, &kubeletConfig, nil) + Expect(err).To(Not(HaveOccurred())) + Expect(&kubeletConfig).To(Equal(newKubeletConfig)) + }) + }) +}) + +func checkKubeAPIServerDeployment(dep *appsv1.Deployment, vpnSeedContainerPresent bool) { + // Check that the kube-apiserver container still exists and contains all needed command line args, + // env vars, and volume mounts + c := extensionswebhook.ContainerWithName(dep.Spec.Template.Spec.Containers, "kube-apiserver") + Expect(c).To(Not(BeNil())) + Expect(c.Command).To(Not(test.ContainElementWithPrefixContaining("--enable-admission-plugins=", "PersistentVolumeLabel", ","))) + Expect(c.Command).To(test.ContainElementWithPrefixContaining("--disable-admission-plugins=", "PersistentVolumeLabel", ",")) + + vpnSeedContainer := extensionswebhook.ContainerWithName(dep.Spec.Template.Spec.Containers, "vpn-seed") + if vpnSeedContainerPresent { + Expect(vpnSeedContainer).To(Not(BeNil())) + } else { + Expect(vpnSeedContainer).To(BeNil()) + } +} + +func checkKubeControllerManagerDeployment(dep *appsv1.Deployment) { + // Check that the kube-controller-manager container still exists and contains all needed command line args, + // env vars, and volume mounts + c := extensionswebhook.ContainerWithName(dep.Spec.Template.Spec.Containers, "kube-controller-manager") + Expect(c).To(Not(BeNil())) +} diff --git a/vendor/github.com/gardener/gardener/extensions/pkg/webhook/controlplane/test/matchers.go b/vendor/github.com/gardener/gardener/extensions/pkg/webhook/controlplane/test/matchers.go new file mode 100644 index 00000000..6440f6df --- /dev/null +++ b/vendor/github.com/gardener/gardener/extensions/pkg/webhook/controlplane/test/matchers.go @@ -0,0 +1,61 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "fmt" + "strings" + + extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook" + + "github.com/onsi/gomega/types" +) + +// ContainElementWithPrefixContaining succeeds if actual contains a string having the given prefix +// and containing the given value in a list separated by sep. +// Actual must be a slice of strings. +func ContainElementWithPrefixContaining(prefix, value, sep string) types.GomegaMatcher { + return &containElementWithPrefixContainingMatcher{ + prefix: prefix, + value: value, + sep: sep, + } +} + +type containElementWithPrefixContainingMatcher struct { + prefix, value, sep string +} + +func (m *containElementWithPrefixContainingMatcher) Match(actual interface{}) (success bool, err error) { + items, ok := actual.([]string) + if !ok { + return false, fmt.Errorf("ContainElementWithPrefixContaining matcher expects []string") + } + i := extensionswebhook.StringWithPrefixIndex(items, m.prefix) + if i < 0 { + return false, nil + } + values := strings.Split(strings.TrimPrefix(items[i], m.prefix), m.sep) + j := extensionswebhook.StringIndex(values, m.value) + return j >= 0, nil +} + +func (m *containElementWithPrefixContainingMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nto contain an element with prefix '%s' containing '%s'", actual, m.prefix, m.value) +} + +func (m *containElementWithPrefixContainingMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nnot to contain an element with prefix '%s' containing '%s'", actual, m.prefix, m.value) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index de9267d9..3b85ccfe 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -16,6 +16,7 @@ github.com/ahmetb/gen-crd-api-reference-docs # github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 github.com/beorn7/perks/quantile # github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f +## explicit github.com/coreos/go-systemd/unit # github.com/cyphar/filepath-securejoin v0.2.2 github.com/cyphar/filepath-securejoin @@ -78,6 +79,7 @@ github.com/gardener/gardener/extensions/pkg/webhook github.com/gardener/gardener/extensions/pkg/webhook/cmd github.com/gardener/gardener/extensions/pkg/webhook/controlplane github.com/gardener/gardener/extensions/pkg/webhook/controlplane/genericmutator +github.com/gardener/gardener/extensions/pkg/webhook/controlplane/test github.com/gardener/gardener/extensions/pkg/webhook/shoot github.com/gardener/gardener/hack github.com/gardener/gardener/hack/.ci @@ -647,8 +649,10 @@ k8s.io/apimachinery/third_party/forked/golang/json k8s.io/apimachinery/third_party/forked/golang/netutil k8s.io/apimachinery/third_party/forked/golang/reflect # k8s.io/apiserver v0.17.6 => k8s.io/apiserver v0.16.8 +## explicit k8s.io/apiserver/pkg/authentication/user # k8s.io/autoscaler v0.0.0-20190805135949-100e91ba756e +## explicit k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta2 # k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible => k8s.io/client-go v0.16.8 ## explicit @@ -812,8 +816,10 @@ k8s.io/kube-openapi/pkg/generators/rules k8s.io/kube-openapi/pkg/util/proto k8s.io/kube-openapi/pkg/util/sets # k8s.io/kubelet v0.17.6 +## explicit k8s.io/kubelet/config/v1beta1 # k8s.io/utils v0.0.0-20200327001022-6496210b90e8 +## explicit k8s.io/utils/buffer k8s.io/utils/integer k8s.io/utils/pointer