diff --git a/pkg/operator/sync.go b/pkg/operator/sync.go index c10c7ab9d5..9e60350b89 100644 --- a/pkg/operator/sync.go +++ b/pkg/operator/sync.go @@ -659,6 +659,15 @@ func newContainers(config *OperatorConfig, features map[string]bool) []corev1.Co corev1.ResourceCPU: resource.MustParse("10m"), }, } + machineControllerResources := corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceMemory: resource.MustParse("20Mi"), + corev1.ResourceCPU: resource.MustParse("10m"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + } args := []string{ "--logtostderr=true", "--v=3", @@ -750,7 +759,7 @@ func newContainers(config *OperatorConfig, features map[string]bool) []corev1.Co Image: config.Controllers.Provider, Command: []string{"/machine-controller-manager"}, Args: machineControllerArgs, - Resources: resources, + Resources: machineControllerResources, Env: append(proxyEnvArgs, corev1.EnvVar{ Name: "NODE_NAME", ValueFrom: &corev1.EnvVarSource{ diff --git a/pkg/operator/sync_test.go b/pkg/operator/sync_test.go index a4f38d798d..87294a54a3 100644 --- a/pkg/operator/sync_test.go +++ b/pkg/operator/sync_test.go @@ -511,3 +511,419 @@ func TestSyncWebhookConfiguration(t *testing.T) { }) } } + +func TestCheckDaemonSetRolloutStatus(t *testing.T) { + testCases := []struct { + name string + daemonset *appsv1.DaemonSet + expectedError error + expectedRequeueAfter time.Duration + }{ + { + name: "DaemonSet fully rolled out", + daemonset: &appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "DaemonSet", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ds", + Namespace: targetNamespace, + Generation: 1, + }, + Status: appsv1.DaemonSetStatus{ + ObservedGeneration: 1, + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 3, + NumberAvailable: 3, + NumberUnavailable: 0, + }, + }, + expectedError: nil, + expectedRequeueAfter: 0, + }, + { + name: "DaemonSet with Generation > ObservedGeneration should requeue", + daemonset: &appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "DaemonSet", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ds", + Namespace: targetNamespace, + Generation: 2, + }, + Status: appsv1.DaemonSetStatus{ + ObservedGeneration: 1, + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 3, + NumberAvailable: 3, + NumberUnavailable: 0, + }, + }, + expectedError: nil, + expectedRequeueAfter: 5 * time.Second, + }, + } + + imagesJSONFile, err := createImagesJSONFromManifest() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Remove(imagesJSONFile); err != nil { + t.Fatal(err) + } + }() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + stopCh := make(chan struct{}) + defer close(stopCh) + optr, err := newFakeOperator([]runtime.Object{tc.daemonset}, nil, nil, imagesJSONFile, nil, stopCh) + if err != nil { + t.Fatal(err) + } + + result, gotErr := optr.checkDaemonSetRolloutStatus(tc.daemonset) + if tc.expectedError != nil && gotErr != nil { + if tc.expectedError.Error() != gotErr.Error() { + t.Errorf("Got error: %v, expected: %v", gotErr, tc.expectedError) + } + } else if tc.expectedError != gotErr { + t.Errorf("Got error: %v, expected: %v", gotErr, tc.expectedError) + } + + if tc.expectedRequeueAfter != result.RequeueAfter { + t.Errorf("Got requeueAfter: %v, expected: %v", result.RequeueAfter, tc.expectedRequeueAfter) + } + }) + } +} +func TestNewKubeProxyContainers(t *testing.T) { + testCases := []struct { + name string + image string + withMHCProxy bool + tlsProfile configv1.TLSProfileSpec + expectedCipherSuitesInArgs bool + expectedPorts map[string]int32 + }{ + { + name: "TLS 1.2 Intermediate profile with cipher suites", + image: "test-image:latest", + withMHCProxy: true, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + MinTLSVersion: configv1.VersionTLS12, + }, + expectedCipherSuitesInArgs: true, + expectedPorts: map[string]int32{ + "kube-rbac-proxy-machineset-mtrc": machineSetExposeMetricsPort, + "kube-rbac-proxy-machine-mtrc": machineExposeMetricsPort, + "kube-rbac-proxy-mhc-mtrc": machineHealthCheckExposeMetricsPort, + }, + }, + { + name: "TLS 1.3 Modern profile without cipher suites", + image: "test-image:latest", + withMHCProxy: false, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + }, + MinTLSVersion: configv1.VersionTLS13, + }, + expectedCipherSuitesInArgs: false, + expectedPorts: map[string]int32{ + "kube-rbac-proxy-machineset-mtrc": machineSetExposeMetricsPort, + "kube-rbac-proxy-machine-mtrc": machineExposeMetricsPort, + }, + }, + { + name: "Empty cipher list", + image: "test-image:latest", + withMHCProxy: false, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{}, + MinTLSVersion: configv1.VersionTLS13, + }, + expectedCipherSuitesInArgs: false, + expectedPorts: map[string]int32{ + "kube-rbac-proxy-machineset-mtrc": machineSetExposeMetricsPort, + "kube-rbac-proxy-machine-mtrc": machineExposeMetricsPort, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + containers := newKubeProxyContainers(tc.image, tc.withMHCProxy, getTLSArgs(tc.tlsProfile)) + + // Verify we get the expected number of containers + g.Expect(containers).To(HaveLen(len(tc.expectedPorts))) + + // Verify each container has the correct TLS args and specific ports + for _, container := range containers { + // Verify basic container properties + g.Expect(container.Image).To(Equal(tc.image)) + + // Verify ports + g.Expect(container.Ports).To(HaveLen(1)) + expectedPort, ok := tc.expectedPorts[container.Name] + g.Expect(ok).To(BeTrue(), "Unexpected container name: %s", container.Name) + g.Expect(container.Ports[0].ContainerPort).To(Equal(expectedPort)) + + // Verify resource requests + g.Expect(container.Resources.Requests).To(HaveKey(corev1.ResourceMemory)) + g.Expect(container.Resources.Requests).To(HaveKey(corev1.ResourceCPU)) + + // Verify volume mounts + g.Expect(container.VolumeMounts).To(HaveLen(2)) + + // Verify TLS args + hasCipherSuitesArg := false + hasTLSMinVersionArg := false + for _, arg := range container.Args { + if strings.HasPrefix(arg, "--tls-cipher-suites=") { + hasCipherSuitesArg = true + } + if strings.HasPrefix(arg, "--tls-min-version=") { + hasTLSMinVersionArg = true + g.Expect(arg).To(HavePrefix("--tls-min-version=" + string(tc.tlsProfile.MinTLSVersion))) + } + } + + g.Expect(hasCipherSuitesArg).To(Equal(tc.expectedCipherSuitesInArgs), + "cipher suites arg presence mismatch for container %s", container.Name) + g.Expect(hasTLSMinVersionArg).To(BeTrue(), "TLS min version arg should be present for container %s", container.Name) + } + }) + } +} + +func TestNewContainersMemoryLimit(t *testing.T) { + g := NewWithT(t) + + config := &OperatorConfig{ + TargetNamespace: targetNamespace, + PlatformType: configv1.AWSPlatformType, + Controllers: Controllers{ + Provider: "provider-image:latest", + MachineSet: "machineset-image:latest", + NodeLink: "nodelink-image:latest", + MachineHealthCheck: "mhc-image:latest", + }, + } + + containers := newContainers(config, nil, nil) + + for _, container := range containers { + g.Expect(container.Resources.Requests.Memory().String()).To(Equal("20Mi"), + "memory request for %s", container.Name) + + if container.Name == "machine-controller" { + g.Expect(container.Resources.Limits.Memory().String()).To(Equal("512Mi"), + "memory limit for %s", container.Name) + } else { + g.Expect(container.Resources.Limits.Memory().IsZero()).To(BeTrue(), + "unexpected memory limit on %s", container.Name) + } + } +} + +func TestNewPodTemplateSpecTLSArgs(t *testing.T) { + testCases := []struct { + name string + config *OperatorConfig + tlsProfile configv1.TLSProfileSpec + tlsAdherencePolicy configv1.TLSAdherencePolicy + expectedTLSProfile configv1.TLSProfileSpec + expectMachineControllerTLSOnBareMetal bool + expectTLSArgsOnProfileConsumers bool + }{ + { + name: "AWS: TLS 1.2 with cipher suites", + config: &OperatorConfig{ + TargetNamespace: targetNamespace, + PlatformType: configv1.AWSPlatformType, + Controllers: Controllers{ + Provider: "provider-image:latest", + MachineSet: "machineset-image:latest", + NodeLink: "nodelink-image:latest", + MachineHealthCheck: "mhc-image:latest", + KubeRBACProxy: "kube-rbac-proxy-image:latest", + }, + }, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + MinTLSVersion: configv1.VersionTLS12, + }, + expectedTLSProfile: configv1.TLSProfileSpec{Ciphers: []string{"ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256"}, MinTLSVersion: configv1.VersionTLS12}, + expectMachineControllerTLSOnBareMetal: false, + tlsAdherencePolicy: configv1.TLSAdherencePolicyStrictAllComponents, + expectTLSArgsOnProfileConsumers: true, + }, + { + name: "GCP: TLS 1.3 without cipher suites", + config: &OperatorConfig{ + TargetNamespace: targetNamespace, + PlatformType: configv1.GCPPlatformType, + Controllers: Controllers{ + Provider: "provider-image:latest", + MachineSet: "machineset-image:latest", + NodeLink: "nodelink-image:latest", + MachineHealthCheck: "", + KubeRBACProxy: "kube-rbac-proxy-image:latest", + }, + }, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{}, + MinTLSVersion: configv1.VersionTLS13, + }, + expectedTLSProfile: *configv1.TLSProfiles[configv1.TLSProfileIntermediateType], + expectMachineControllerTLSOnBareMetal: false, + tlsAdherencePolicy: configv1.TLSAdherencePolicyLegacyAdheringComponentsOnly, + expectTLSArgsOnProfileConsumers: true, + }, + { + name: "BareMetal: TLS args passed to machine-controller for Metal3Remediation webhooks", + config: &OperatorConfig{ + TargetNamespace: targetNamespace, + PlatformType: configv1.BareMetalPlatformType, + Controllers: Controllers{ + Provider: "provider-image:latest", + MachineSet: "machineset-image:latest", + NodeLink: "nodelink-image:latest", + MachineHealthCheck: "mhc-image:latest", + KubeRBACProxy: "kube-rbac-proxy-image:latest", + }, + }, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + MinTLSVersion: configv1.VersionTLS12, + }, + expectedTLSProfile: configv1.TLSProfileSpec{Ciphers: []string{"ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256"}, MinTLSVersion: configv1.VersionTLS12}, + expectMachineControllerTLSOnBareMetal: true, + tlsAdherencePolicy: configv1.TLSAdherencePolicyStrictAllComponents, + expectTLSArgsOnProfileConsumers: true, + }, + { + name: "AWS: no opinion applies default profile TLS args through pod template", + config: &OperatorConfig{ + TargetNamespace: targetNamespace, + PlatformType: configv1.AWSPlatformType, + Controllers: Controllers{ + Provider: "provider-image:latest", + MachineSet: "machineset-image:latest", + NodeLink: "nodelink-image:latest", + MachineHealthCheck: "mhc-image:latest", + KubeRBACProxy: "kube-rbac-proxy-image:latest", + }, + }, + tlsProfile: configv1.TLSProfileSpec{ + Ciphers: []string{ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + MinTLSVersion: configv1.VersionTLS12, + }, + expectedTLSProfile: *configv1.TLSProfiles[configv1.TLSProfileIntermediateType], + expectMachineControllerTLSOnBareMetal: false, + tlsAdherencePolicy: configv1.TLSAdherencePolicyNoOpinion, + expectTLSArgsOnProfileConsumers: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + configForPodTemplate := *tc.config + configForPodTemplate.TLSProfile = tc.tlsProfile + configForPodTemplate.TLSAdherencePolicy = tc.tlsAdherencePolicy + podTemplate := newPodTemplateSpec(&configForPodTemplate, map[string]bool{}) + + containerArgs := map[string][]string{} + for _, container := range podTemplate.Spec.Containers { + containerArgs[container.Name] = container.Args + } + + g.Expect(containerArgs).To(HaveKey("machineset-controller")) + g.Expect(containerArgs).To(HaveKey("machine-controller")) + g.Expect(containerArgs).To(HaveKey("nodelink-controller")) + g.Expect(containerArgs).To(HaveKey("kube-rbac-proxy-machineset-mtrc")) + g.Expect(containerArgs).To(HaveKey("kube-rbac-proxy-machine-mtrc")) + + if tc.config.Controllers.MachineHealthCheck != "" { + g.Expect(containerArgs).To(HaveKey("machine-healthcheck-controller")) + g.Expect(containerArgs).To(HaveKey("kube-rbac-proxy-mhc-mtrc")) + } + + expectedTLSArgs := getTLSArgs(tc.expectedTLSProfile) + assertTLSArgs := func(args []string, shouldContain bool) { + joined := strings.Join(args, " ") + for _, expectedTLSArg := range expectedTLSArgs { + if shouldContain { + g.Expect(joined).To(ContainSubstring(expectedTLSArg)) + } else { + g.Expect(joined).ToNot(ContainSubstring(expectedTLSArg)) + } + } + } + + // machineset-controller and kube-rbac-proxy containers always receive TLS args + // from either the cluster profile (strict) or the default profile. + assertTLSArgs(containerArgs["machineset-controller"], tc.expectTLSArgsOnProfileConsumers) + assertTLSArgs(containerArgs["kube-rbac-proxy-machineset-mtrc"], tc.expectTLSArgsOnProfileConsumers) + assertTLSArgs(containerArgs["kube-rbac-proxy-machine-mtrc"], tc.expectTLSArgsOnProfileConsumers) + if tc.config.Controllers.MachineHealthCheck != "" { + assertTLSArgs(containerArgs["kube-rbac-proxy-mhc-mtrc"], tc.expectTLSArgsOnProfileConsumers) + } + + // machine-controller gets TLS args only on BareMetal. + expectMachineControllerTLSArgs := tc.expectTLSArgsOnProfileConsumers && tc.expectMachineControllerTLSOnBareMetal + assertTLSArgs(containerArgs["machine-controller"], expectMachineControllerTLSArgs) + + // nodelink-controller and machine-healthcheck-controller never receive TLS args. + assertTLSArgs(containerArgs["nodelink-controller"], false) + if tc.config.Controllers.MachineHealthCheck != "" { + assertTLSArgs(containerArgs["machine-healthcheck-controller"], false) + } + }) + } +} + +func TestResolveTLSProfile(t *testing.T) { + g := NewWithT(t) + + clusterTLSProfile := configv1.TLSProfileSpec{ + Ciphers: []string{ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + MinTLSVersion: configv1.VersionTLS12, + } + defaultTLSProfile := *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] + + g.Expect(resolveTLSProfile(clusterTLSProfile, configv1.TLSAdherencePolicyStrictAllComponents)).To(Equal(clusterTLSProfile)) + g.Expect(resolveTLSProfile(clusterTLSProfile, configv1.TLSAdherencePolicyNoOpinion)).To(Equal(defaultTLSProfile)) + g.Expect(resolveTLSProfile(clusterTLSProfile, configv1.TLSAdherencePolicyLegacyAdheringComponentsOnly)).To(Equal(defaultTLSProfile)) +}