diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index 653e0c021709..555ae444d079 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -203,6 +203,8 @@ spec: - /etc/konnectivity/cluster/tls.crt - --cluster-key - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt - --server-cert - /etc/konnectivity/server/tls.crt - --server-key diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/GCP/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/GCP/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index baf17db526df..e1e43ac24d2b 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/GCP/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/GCP/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -208,6 +208,8 @@ spec: - /etc/konnectivity/cluster/tls.crt - --cluster-key - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt - --server-cert - /etc/konnectivity/server/tls.crt - --server-key diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/IBMCloud/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/IBMCloud/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index cc3501739e50..c3641ba70e9c 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/IBMCloud/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/IBMCloud/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -198,6 +198,8 @@ spec: - /etc/konnectivity/cluster/tls.crt - --cluster-key - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt - --server-cert - /etc/konnectivity/server/tls.crt - --server-key diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/ModernTLS/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/ModernTLS/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml new file mode 100644 index 000000000000..048aa923e467 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/ModernTLS/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -0,0 +1,600 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + hypershift.openshift.io/managed-by: control-plane-operator + name: kube-apiserver + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +spec: + replicas: 3 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: kube-apiserver + strategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes: bootstrap-manifests,logs,tmp-dir + component.hypershift.openshift.io/config-hash: 0fd3eed819dc307e1d8949e2360eec75741638a5741638a5741638a5741638a5860ae2958b110e88a2a4098ca2a4098ce7e69167 + hypershift.openshift.io/release-image: quay.io/openshift-release-dev/ocp-release:4.16.10-x86_64 + labels: + app: kube-apiserver + hypershift.openshift.io/control-plane-component: kube-apiserver + hypershift.openshift.io/hosted-control-plane: hcp-namespace + hypershift.openshift.io/request-serving-component: "true" + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: hypershift.openshift.io/control-plane + operator: In + values: + - "true" + weight: 50 + - preference: + matchExpressions: + - key: hypershift.openshift.io/cluster + operator: In + values: + - hcp-namespace + weight: 100 + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + hypershift.openshift.io/hosted-control-plane: hcp-namespace + topologyKey: kubernetes.io/hostname + weight: 100 + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: kube-apiserver + hypershift.openshift.io/control-plane-component: kube-apiserver + hypershift.openshift.io/hosted-control-plane: hcp-namespace + hypershift.openshift.io/request-serving-component: "true" + topologyKey: topology.kubernetes.io/zone + - labelSelector: + matchLabels: + app: kube-apiserver + hypershift.openshift.io/control-plane-component: kube-apiserver + hypershift.openshift.io/hosted-control-plane: hcp-namespace + hypershift.openshift.io/request-serving-component: "true" + topologyKey: kubernetes.io/hostname + automountServiceAccountToken: false + containers: + - args: + - kube-apiserver + - --openshift-config=/etc/kubernetes/config/config.json + - --v=2 + command: + - hyperkube + env: + - name: HOST_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + image: hyperkube + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 6 + httpGet: + path: livez?exclude=etcd + port: client + scheme: HTTPS + initialDelaySeconds: 300 + periodSeconds: 180 + successThreshold: 1 + timeoutSeconds: 160 + name: kube-apiserver + ports: + - containerPort: 6443 + name: client + protocol: TCP + readinessProbe: + failureThreshold: 18 + httpGet: + path: readyz + port: client + scheme: HTTPS + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 + resources: + requests: + cpu: 350m + memory: 2Gi + securityContext: + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /etc/kubernetes/certs/aggregator-ca + name: aggregator-ca + - mountPath: /etc/kubernetes/certs/aggregator + name: aggregator-crt + - mountPath: /etc/kubernetes/audit + name: audit-config + - mountPath: /etc/kubernetes/auth + name: auth-config + - mountPath: /etc/kubernetes/auth-token-webhook + name: auth-token-webhook-config + - mountPath: /etc/kubernetes/certs/client-ca + name: client-ca + - mountPath: /etc/kubernetes/egress-selector + name: egress-selector-config + - mountPath: /etc/kubernetes/certs/etcd-ca + name: etcd-ca + - mountPath: /etc/kubernetes/certs/etcd + name: etcd-client-crt + - mountPath: /etc/kubernetes/config + name: kas-config + - mountPath: /etc/kubernetes/certs/konnectivity-ca + name: konnectivity-ca + - mountPath: /etc/kubernetes/certs/konnectivity-client + name: konnectivity-client + - mountPath: /etc/kubernetes/certs/kubelet-ca + name: kubelet-client-ca + - mountPath: /etc/kubernetes/certs/kubelet + name: kubelet-client-crt + - mountPath: /var/log/kube-apiserver + name: logs + - mountPath: /etc/kubernetes/oauth + name: oauth-metadata + - mountPath: /etc/kubernetes/certs/server + name: server-crt + - mountPath: /etc/kubernetes/certs/server-private + name: server-private-crt + - mountPath: /etc/kubernetes/secrets/svcacct-key + name: svcacct-key + - mountPath: /tmp + name: tmp-dir + workingDir: /var/log/kube-apiserver + - command: + - /usr/bin/control-plane-operator + - kas-bootstrap + - --resources-path + - /work + env: + - name: KUBECONFIG + value: /var/secrets/kas-bootstrap-container-kubeconfig/kubeconfig + - name: KUBECONFIG_ADMIN + value: /var/secrets/localhost-kubeconfig/kubeconfig + image: controlplane-operator + imagePullPolicy: IfNotPresent + name: bootstrap + resources: + requests: + cpu: 10m + memory: 10Mi + securityContext: + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /work + name: bootstrap-manifests + - mountPath: /var/secrets/localhost-kubeconfig + name: localhost-kubeconfig + - mountPath: /var/secrets/kas-bootstrap-container-kubeconfig + name: kas-bootstrap-container-kubeconfig + - mountPath: /tmp + name: tmp-dir + - args: + - --logtostderr=true + - --log-file-max-size=0 + - --cluster-cert + - /etc/konnectivity/cluster/tls.crt + - --cluster-key + - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt + - --server-cert + - /etc/konnectivity/server/tls.crt + - --server-key + - /etc/konnectivity/server/tls.key + - --server-ca-cert + - /etc/konnectivity/ca/ca.crt + - --server-port + - "8090" + - --agent-port + - "8091" + - --health-port + - "2041" + - --admin-port=8093 + - --mode=http-connect + - --proxy-strategies=destHost,defaultRoute + - --keepalive-time + - 30s + - --frontend-keepalive-time + - 30s + - --server-count + - "3" + - --tls-min-version=VersionTLS13 + command: + - /usr/bin/proxy-server + image: apiserver-network-proxy + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - sleep 70 + livenessProbe: + failureThreshold: 3 + httpGet: + path: healthz + port: 2041 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 60 + successThreshold: 1 + timeoutSeconds: 30 + name: konnectivity-server + readinessProbe: + failureThreshold: 3 + httpGet: + path: healthz + port: 2041 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + requests: + cpu: 10m + memory: 50Mi + securityContext: + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /etc/konnectivity/cluster + name: cluster-certs + - mountPath: /etc/konnectivity/ca + name: konnectivity-ca + - mountPath: /etc/konnectivity/server + name: server-certs + - mountPath: /tmp + name: tmp-dir + - args: + - -c + - | + set -o errexit + set -o nounset + set -o pipefail + + function cleanup() { + pkill -P $$$ + wait + exit + } + trap cleanup SIGTERM + + /usr/bin/tail -c+1 -F /var/log/kube-apiserver/audit.log & + wait $! + command: + - /bin/bash + image: cli + imagePullPolicy: IfNotPresent + name: audit-logs + resources: + requests: + cpu: 5m + memory: 10Mi + securityContext: + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /var/log/kube-apiserver + name: logs + - mountPath: /tmp + name: tmp-dir + - command: + - /usr/bin/aws-pod-identity-webhook + - --annotation-prefix=eks.amazonaws.com + - --in-cluster=false + - --kubeconfig=/var/run/app/kubeconfig/kubeconfig + - --logtostderr + - --port=4443 + - --aws-default-region= + - --tls-cert=/var/run/app/certs/tls.crt + - --tls-key=/var/run/app/certs/tls.key + - --token-audience=openshift + - --tls-min-version=VersionTLS13 + image: aws-pod-identity-webhook + imagePullPolicy: IfNotPresent + name: aws-pod-identity-webhook + resources: + requests: + cpu: 10m + memory: 25Mi + securityContext: + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /var/run/app/certs + name: aws-pod-identity-webhook-serving-certs + - mountPath: /var/run/app/kubeconfig + name: aws-pod-identity-webhook-kubeconfig + - mountPath: /tmp + name: tmp-dir + initContainers: + - command: + - /bin/sh + - -c + - 'echo ''Image for kube-apiserver pre-pulled: hyperkube''; exit 0' + image: hyperkube + imagePullPolicy: IfNotPresent + name: pre-pull-image-kube-apiserver + resources: {} + terminationMessagePolicy: FallbackToLogsOnError + - args: + - -c + - | + #!/bin/sh + cd /tmp + mkdir input output manifests + + touch /tmp/manifests/99_feature-gate.yaml + cat </tmp/manifests/99_feature-gate.yaml + $(FEATURE_GATE_YAML) + EOF + + touch /tmp/manifests/hcco-rolebinding.yaml + cat </tmp/manifests/hcco-rolebinding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: hcco-cluster-admin + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: system:hosted-cluster-config + EOF + + touch /tmp/manifests/00_kas-bootstrap-container-rolebinding.yaml + cat </tmp/manifests/00_kas-bootstrap-container-rolebinding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: kas-bootstrap-container-cluster-admin + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: system:kas-bootstrap-container + EOF + + /usr/bin/render \ + --asset-output-dir /tmp/output \ + --rendered-manifest-dir=/tmp/manifests \ + --cluster-profile=ibm-cloud-managed \ + --payload-version=$(PAYLOAD_VERSION) + cp /tmp/output/manifests/* /work + cp /tmp/manifests/* /work + command: + - /bin/bash + env: + - name: PAYLOAD_VERSION + value: 4.18.0 + - name: FEATURE_GATE_YAML + value: | + apiVersion: config.openshift.io/v1 + kind: FeatureGate + metadata: + name: cluster + spec: {} + status: + featureGates: null + image: cluster-config-api + imagePullPolicy: IfNotPresent + name: init-bootstrap-render + resources: + requests: + cpu: 30m + memory: 50Mi + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /work + name: bootstrap-manifests + - args: + - -c + - | + #!/bin/sh + cd /tmp + mkdir input output manifests + + /usr/bin/authentication-operator render \ + --asset-output-dir /tmp/output \ + --rendered-manifest-dir=/tmp/manifests \ + --cluster-profile=ibm-cloud-managed \ + --payload-version=$(PAYLOAD_VERSION) + + cp /tmp/output/* /work + command: + - /bin/bash + env: + - name: PAYLOAD_VERSION + value: 4.18.0 + - name: FEATURE_GATE_YAML + value: | + apiVersion: config.openshift.io/v1 + kind: FeatureGate + metadata: + name: cluster + spec: {} + status: + featureGates: null + image: cluster-authentication-operator + imagePullPolicy: IfNotPresent + name: init-auth-bootstrap-render + resources: + requests: + cpu: 30m + memory: 50Mi + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /work + name: bootstrap-manifests + - args: + - -c + - | + #!/bin/sh + while ! nslookup etcd-client.$(POD_NAMESPACE).svc; do sleep 1; done + command: + - /bin/bash + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: cli + imagePullPolicy: IfNotPresent + name: wait-for-etcd + resources: {} + terminationMessagePolicy: FallbackToLogsOnError + priorityClassName: hypershift-api-critical + schedulerName: default-scheduler + terminationGracePeriodSeconds: 135 + tolerations: + - effect: NoSchedule + key: hypershift.openshift.io/control-plane + operator: Equal + value: "true" + - effect: NoSchedule + key: hypershift.openshift.io/cluster + operator: Equal + value: hcp-namespace + volumes: + - emptyDir: {} + name: bootstrap-manifests + - name: localhost-kubeconfig + secret: + defaultMode: 416 + secretName: localhost-kubeconfig + - name: kas-bootstrap-container-kubeconfig + secret: + defaultMode: 416 + secretName: kas-bootstrap-container-kubeconfig + - emptyDir: {} + name: logs + - configMap: + defaultMode: 420 + name: kas-config + name: kas-config + - configMap: + defaultMode: 420 + name: auth-config + name: auth-config + - configMap: + defaultMode: 420 + name: kas-audit-config + name: audit-config + - configMap: + defaultMode: 420 + name: konnectivity-ca-bundle + name: konnectivity-ca + - name: server-crt + secret: + defaultMode: 416 + secretName: kas-server-crt + - name: server-private-crt + secret: + defaultMode: 416 + secretName: kas-server-private-crt + - name: aggregator-crt + secret: + defaultMode: 416 + secretName: kas-aggregator-crt + - configMap: + defaultMode: 420 + name: aggregator-client-ca + name: aggregator-ca + - name: svcacct-key + secret: + defaultMode: 416 + secretName: sa-signing-key + - configMap: + defaultMode: 420 + name: etcd-ca + name: etcd-ca + - name: etcd-client-crt + secret: + defaultMode: 416 + secretName: etcd-client-tls + - configMap: + defaultMode: 420 + name: oauth-metadata + name: oauth-metadata + - name: auth-token-webhook-config + secret: + defaultMode: 416 + secretName: kas-authentication-token-webhook-config + - configMap: + defaultMode: 420 + name: client-ca + name: client-ca + - name: kubelet-client-crt + secret: + defaultMode: 416 + secretName: kas-kubelet-client-crt + - configMap: + defaultMode: 420 + name: client-ca + name: kubelet-client-ca + - name: konnectivity-client + secret: + defaultMode: 416 + secretName: konnectivity-client + - configMap: + defaultMode: 420 + name: kas-egress-selector-config + name: egress-selector-config + - name: kubeconfig + secret: + defaultMode: 416 + secretName: localhost-kubeconfig + - name: server-certs + secret: + defaultMode: 416 + secretName: konnectivity-server + - name: cluster-certs + secret: + defaultMode: 416 + secretName: konnectivity-cluster + - name: aws-pod-identity-webhook-serving-certs + secret: + defaultMode: 416 + secretName: aws-pod-identity-webhook-serving-cert + - name: aws-pod-identity-webhook-kubeconfig + secret: + defaultMode: 416 + secretName: aws-pod-identity-webhook-kubeconfig + - emptyDir: {} + name: tmp-dir +status: {} diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index 90862ea7d906..f447d5fcc8f8 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -198,6 +198,8 @@ spec: - /etc/konnectivity/cluster/tls.crt - --cluster-key - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt - --server-cert - /etc/konnectivity/server/tls.crt - --server-key diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index a624195bfc34..803079f10355 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -198,6 +198,8 @@ spec: - /etc/konnectivity/cluster/tls.crt - --cluster-key - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt - --server-cert - /etc/konnectivity/server/tls.crt - --server-key diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/assets_test.go b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/assets_test.go new file mode 100644 index 000000000000..2aa460cb003f --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/assets_test.go @@ -0,0 +1,420 @@ +package assets + +import ( + "slices" + "testing" + + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestLoadDeploymentManifest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + validate func(g Gomega, deployment *appsv1.Deployment, err error) + }{ + { + name: "When loading a valid deployment manifest, it should decode successfully", + componentName: "aws-cloud-controller-manager", + validate: func(g Gomega, deployment *appsv1.Deployment, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(deployment).ToNot(BeNil()) + g.Expect(deployment.Kind).To(Equal("Deployment")) + g.Expect(deployment.Name).To(Equal("cloud-controller-manager")) + }, + }, + { + name: "When component name does not exist, it should return an error", + componentName: "nonexistent-component", + validate: func(g Gomega, deployment *appsv1.Deployment, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(deployment).To(BeNil()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + deployment, err := LoadDeploymentManifest(tc.componentName) + tc.validate(g, deployment, err) + }) + } +} + +func TestKonnectivityServerAuthenticatesAgents(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + deployment, err := LoadDeploymentManifest("kube-apiserver") + if err != nil { + t.Fatalf("failed to load kube-apiserver deployment manifest: %v", err) + } + if deployment == nil { + t.Fatal("kube-apiserver deployment manifest is nil") + } + + var konnectivityServer *corev1.Container + for i := range deployment.Spec.Template.Spec.Containers { + if deployment.Spec.Template.Spec.Containers[i].Name == "konnectivity-server" { + konnectivityServer = &deployment.Spec.Template.Spec.Containers[i] + break + } + } + + if konnectivityServer == nil { + t.Fatal("konnectivity-server container not found") + } + + clusterCAFlagIndex := slices.Index(konnectivityServer.Args, "--cluster-ca-cert") + if clusterCAFlagIndex == -1 || clusterCAFlagIndex+1 >= len(konnectivityServer.Args) { + t.Fatal("--cluster-ca-cert flag or its value not found") + } + g.Expect(konnectivityServer.Args[clusterCAFlagIndex+1]).To(Equal("/etc/konnectivity/ca/ca.crt")) +} + +func TestLoadStatefulSetManifest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + validate func(g Gomega, sts *appsv1.StatefulSet, err error) + }{ + { + name: "When loading a valid statefulset manifest, it should decode successfully", + componentName: "etcd", + validate: func(g Gomega, sts *appsv1.StatefulSet, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(sts).ToNot(BeNil()) + g.Expect(sts.Kind).To(Equal("StatefulSet")) + g.Expect(sts.Name).To(Equal("etcd")) + g.Expect(sts.Spec.Selector.MatchLabels["app"]).To(Equal("etcd")) + g.Expect(sts.Spec.Template.Labels["app"]).To(Equal("etcd")) + g.Expect(sts.Spec.ServiceName).To(Equal("etcd-discovery")) + }, + }, + { + name: "When component name does not exist, it should return an error", + componentName: "nonexistent-component", + validate: func(g Gomega, sts *appsv1.StatefulSet, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(sts).To(BeNil()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + sts, err := LoadStatefulSetManifest(tc.componentName) + tc.validate(g, sts, err) + }) + } +} + +func TestLoadCronJobManifest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + validate func(g Gomega, cronJob *batchv1.CronJob, err error) + }{ + { + name: "When loading a valid cronjob manifest, it should decode successfully", + componentName: "olm-collect-profiles", + validate: func(g Gomega, cronJob *batchv1.CronJob, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cronJob).ToNot(BeNil()) + g.Expect(cronJob.Kind).To(Equal("CronJob")) + g.Expect(cronJob.Name).To(Equal("olm-collect-profiles")) + }, + }, + { + name: "When component name does not exist, it should return an error", + componentName: "nonexistent-component", + validate: func(g Gomega, cronJob *batchv1.CronJob, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(cronJob).To(BeNil()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + cronJob, err := LoadCronJobManifest(tc.componentName) + tc.validate(g, cronJob, err) + }) + } +} + +func TestLoadJobManifest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + validate func(g Gomega, job *batchv1.Job, err error) + }{ + { + name: "When loading a valid job manifest, it should decode successfully", + componentName: "featuregate-generator", + validate: func(g Gomega, job *batchv1.Job, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(job).ToNot(BeNil()) + g.Expect(job.Kind).To(Equal("Job")) + g.Expect(job.Name).To(Equal("featuregate-generator")) + }, + }, + { + name: "When component name does not exist, it should return an error", + componentName: "nonexistent-component", + validate: func(g Gomega, job *batchv1.Job, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(job).To(BeNil()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + job, err := LoadJobManifest(tc.componentName) + tc.validate(g, job, err) + }) + } +} + +func TestLoadManifest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + fileName string + validate func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) + }{ + { + name: "When loading a service manifest, it should decode successfully", + componentName: "cluster-autoscaler", + fileName: "serviceaccount.yaml", + validate: func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + g.Expect(gvk).ToNot(BeNil()) + g.Expect(gvk.Kind).To(Equal("ServiceAccount")) + sa, ok := obj.(*corev1.ServiceAccount) + g.Expect(ok).To(BeTrue()) + g.Expect(sa.Name).To(Equal("cluster-autoscaler")) + }, + }, + { + name: "When loading a role manifest, it should decode successfully", + componentName: "cluster-autoscaler", + fileName: "role.yaml", + validate: func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + g.Expect(gvk).ToNot(BeNil()) + g.Expect(gvk.Kind).To(Equal("Role")) + }, + }, + { + name: "When file does not exist, it should return an error", + componentName: "cluster-autoscaler", + fileName: "nonexistent.yaml", + validate: func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(obj).To(BeNil()) + g.Expect(gvk).To(BeNil()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + obj, gvk, err := LoadManifest(tc.componentName, tc.fileName) + tc.validate(g, obj, gvk, err) + }) + } +} + +func TestLoadManifestInto(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + fileName string + into client.Object + validate func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) + }{ + { + name: "When loading into a pre-allocated ServiceAccount, it should populate the object", + componentName: "cluster-autoscaler", + fileName: "serviceaccount.yaml", + into: &corev1.ServiceAccount{}, + validate: func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + g.Expect(gvk).ToNot(BeNil()) + g.Expect(gvk.Kind).To(Equal("ServiceAccount")) + sa, ok := obj.(*corev1.ServiceAccount) + g.Expect(ok).To(BeTrue()) + g.Expect(sa.Name).To(Equal("cluster-autoscaler")) + }, + }, + { + name: "When loading into nil, it should create a new object", + componentName: "cluster-autoscaler", + fileName: "serviceaccount.yaml", + into: nil, + validate: func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + g.Expect(gvk).ToNot(BeNil()) + g.Expect(gvk.Kind).To(Equal("ServiceAccount")) + }, + }, + { + name: "When file does not exist, it should return an error", + componentName: "cluster-autoscaler", + fileName: "nonexistent.yaml", + into: &corev1.ServiceAccount{}, + validate: func(g Gomega, obj client.Object, gvk *schema.GroupVersionKind, err error) { + g.Expect(err).To(HaveOccurred()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + obj, gvk, err := LoadManifestInto(tc.componentName, tc.fileName, tc.into) + tc.validate(g, obj, gvk, err) + }) + } +} + +func TestForEachManifest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + componentName string + validate func(g Gomega, manifestNames []string, err error) + }{ + { + name: "When iterating over cluster-autoscaler manifests, it should skip deployment and call action for others", + componentName: "cluster-autoscaler", + validate: func(g Gomega, manifestNames []string, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(manifestNames).To(ContainElement("serviceaccount.yaml")) + g.Expect(manifestNames).To(ContainElement("role.yaml")) + g.Expect(manifestNames).To(ContainElement("rolebinding.yaml")) + g.Expect(manifestNames).To(ContainElement("podmonitor.yaml")) + g.Expect(manifestNames).ToNot(ContainElement("deployment.yaml")) + }, + }, + { + name: "When iterating over etcd manifests, it should skip statefulset and call action for others", + componentName: "etcd", + validate: func(g Gomega, manifestNames []string, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(manifestNames).To(ContainElement("service.yaml")) + g.Expect(manifestNames).To(ContainElement("discovery-service.yaml")) + g.Expect(manifestNames).ToNot(ContainElement("statefulset.yaml")) + }, + }, + { + name: "When iterating over openshift-controller-manager manifests, it should skip deployment and call action for others", + componentName: "openshift-controller-manager", + validate: func(g Gomega, manifestNames []string, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(manifestNames).To(ContainElement("config.yaml")) + g.Expect(manifestNames).To(ContainElement("service.yaml")) + g.Expect(manifestNames).ToNot(ContainElement("deployment.yaml")) + }, + }, + { + name: "When iterating over featuregate-generator manifests, it should skip job and call action for others", + componentName: "featuregate-generator", + validate: func(g Gomega, manifestNames []string, err error) { + g.Expect(err).ToNot(HaveOccurred()) + // featuregate-generator only has job.yaml, so no other manifests + g.Expect(manifestNames).To(BeEmpty()) + }, + }, + { + name: "When component does not exist, it should return an error", + componentName: "nonexistent-component", + validate: func(g Gomega, manifestNames []string, err error) { + g.Expect(err).To(HaveOccurred()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + var manifestNames []string + err := ForEachManifest(tc.componentName, func(manifestName string) error { + manifestNames = append(manifestNames, manifestName) + return nil + }) + + tc.validate(g, manifestNames, err) + }) + } +} + +func TestForEachManifestWithActionError(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + expectedErr := &testError{msg: "action failed"} + + err := ForEachManifest("cluster-autoscaler", func(manifestName string) error { + if manifestName == "role.yaml" { + return expectedErr + } + return nil + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(Equal(expectedErr)) +} + +type testError struct { + msg string +} + +func (e *testError) Error() string { + return e.msg +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/deployment.yaml index 95e499986a93..8f7383e4eb9b 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/deployment.yaml @@ -128,6 +128,8 @@ spec: - /etc/konnectivity/cluster/tls.crt - --cluster-key - /etc/konnectivity/cluster/tls.key + - --cluster-ca-cert + - /etc/konnectivity/ca/ca.crt - --server-cert - /etc/konnectivity/server/tls.crt - --server-key