diff --git a/CHANGELOG.md b/CHANGELOG.md index 995ea9e64..12d93d2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ ## Unreleased +> Release date: TBA + ### Added - Add `ExternalTrafficPolicy` to `DataPlane`'s `ServiceOptions` @@ -40,6 +42,10 @@ [#246](https://github.com/Kong/gateway-operator/pull/246) - `Gateway`s' listeners now have their `attachedRoutes` count filled in in status. [#251](https://github.com/Kong/gateway-operator/pull/251) +- Detect when `ControlPlane` has its admission webhook disabled via + `CONTROLLER_ADMISSION_WEBHOOK_LISTEN` environment variable and ensure that + relevant webhook resources are not created/deleted. + [#326](https://github.com/Kong/gateway-operator/pull/326) ### Fixes diff --git a/config/samples/gateway-with-disabled-controlplane-admission-webhook.yaml b/config/samples/gateway-with-disabled-controlplane-admission-webhook.yaml new file mode 100644 index 000000000..f8816cc06 --- /dev/null +++ b/config/samples/gateway-with-disabled-controlplane-admission-webhook.yaml @@ -0,0 +1,111 @@ +apiVersion: v1 +kind: Service +metadata: + name: echo +spec: + ports: + - protocol: TCP + name: http + port: 80 + targetPort: http + selector: + app: echo +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: echo + name: echo +spec: + selector: + matchLabels: + app: echo + template: + metadata: + labels: + app: echo + spec: + containers: + - name: echo + image: registry.k8s.io/e2e-test-images/agnhost:2.40 + command: + - /agnhost + - netexec + - --http-port=8080 + ports: + - containerPort: 8080 + name: http + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP +--- +kind: GatewayConfiguration +apiVersion: gateway-operator.konghq.com/v1beta1 +metadata: + name: kong + namespace: default +spec: + dataPlaneOptions: + deployment: + podTemplateSpec: + spec: + containers: + - name: proxy + # renovate: datasource=docker versioning=docker + image: kong/kong-gateway:3.7 + readinessProbe: + initialDelaySeconds: 1 + periodSeconds: 1 + controlPlaneOptions: + deployment: + podTemplateSpec: + spec: + containers: + - name: controller + # renovate: datasource=docker versioning=docker + image: kong/kubernetes-ingress-controller:3.1.6 + readinessProbe: + initialDelaySeconds: 1 + periodSeconds: 1 + env: + - name: CONTROLLER_ADMISSION_WEBHOOK_LISTEN + value: "off" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: kong +spec: + controllerName: konghq.com/gateway-operator + parametersRef: + group: gateway-operator.konghq.com + kind: GatewayConfiguration + name: kong + namespace: default +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: kong + namespace: default +spec: + gatewayClassName: kong + listeners: + - name: http + protocol: HTTP + port: 80 diff --git a/controller/controlplane/controller.go b/controller/controlplane/controller.go index f7433f610..4728c4a6d 100644 --- a/controller/controlplane/controller.go +++ b/controller/controlplane/controller.go @@ -63,6 +63,9 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { validatinWebhookConfigurationOwnerPredicate.UpdateFunc = func(e event.UpdateEvent) bool { return r.validatingWebhookConfigurationHasControlPlaneOwner(e.ObjectOld) } + validatinWebhookConfigurationOwnerPredicate.DeleteFunc = func(e event.DeleteEvent) bool { + return r.validatingWebhookConfigurationHasControlPlaneOwner(e.Object) + } return ctrl.NewControllerManagedBy(mgr). // watch ControlPlane objects @@ -285,7 +288,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } changed := controlplane.SetDefaults( &cp.Spec.ControlPlaneOptions, - nil, defaultArgs) if changed { log.Debug(logger, "updating ControlPlane resource after defaults are set since resource has changed", cp) @@ -361,43 +363,22 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, nil // requeue will be triggered by the creation or update of the owned object } - log.Trace(logger, "creating admission webhook service", cp) - res, admissionWebhookService, err := r.ensureAdmissionWebhookService(ctx, r.Client, cp) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to ensure admission webhook service: %w", err) - } - if res != op.Noop { - log.Debug(logger, "admission webhook service created/updated", cp) - return ctrl.Result{}, nil // requeue will be triggered by the creation or update of the owned object - } - - log.Trace(logger, "creating admission webhook certificate", cp) - res, admissionWebhookCertificateSecret, err := r.ensureAdmissionWebhookCertificateSecret(ctx, cp, admissionWebhookService) - if err != nil { - return ctrl.Result{}, err - } - if res != op.Noop { - log.Debug(logger, "admission webhook certificate created/updated", cp) - return ctrl.Result{}, nil // requeue will be triggered by the creation or update of the owned object + deploymentParams := ensureDeploymentParams{ + ControlPlane: cp, + ServiceAccountName: controlplaneServiceAccount.Name, + AdminMTLSCertSecretName: adminCertificate.Name, } - log.Trace(logger, "creating admission webhook configuration", cp) - res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, admissionWebhookCertificateSecret, admissionWebhookService.Name) + admissionWebhookCertificateSecretName, res, err := r.ensureWebhookResources(ctx, logger, cp) if err != nil { - return ctrl.Result{}, err - } - if res != op.Noop { - log.Debug(logger, "ValidatingWebhookConfiguration created/updated", cp) - return ctrl.Result{}, nil // requeue will be triggered by the creation or update of the owned object + return ctrl.Result{}, fmt.Errorf("failed to ensure webhook resources: %w", err) + } else if res != op.Noop { + return ctrl.Result{Requeue: true, RequeueAfter: requeueWithoutBackoff}, nil } + deploymentParams.AdmissionWebhookCertSecretName = admissionWebhookCertificateSecretName log.Trace(logger, "looking for existing Deployments for ControlPlane resource", cp) - res, controlplaneDeployment, err := r.ensureDeployment(ctx, logger, ensureDeploymentParams{ - ControlPlane: cp, - ServiceAccountName: controlplaneServiceAccount.Name, - AdminMTLSCertSecretName: adminCertificate.Name, - AdmissionWebhookCertSecretName: admissionWebhookCertificateSecret.Name, - }) + res, controlplaneDeployment, err := r.ensureDeployment(ctx, logger, deploymentParams) if err != nil { return ctrl.Result{}, err } @@ -489,3 +470,80 @@ func (r *Reconciler) patchStatus(ctx context.Context, logger logr.Logger, update return ctrl.Result{}, nil } + +func (r *Reconciler) ensureWebhookResources( + ctx context.Context, logger logr.Logger, cp *operatorv1beta1.ControlPlane, +) (string, op.Result, error) { + webhookEnabled := isAdmissionWebhookEnabled(ctx, r.Client, logger, cp) + if !webhookEnabled { + log.Debug(logger, "admission webhook disabled, ensuring admission webhook resources are not present", cp) + } else { + log.Debug(logger, "admission webhook enabled, enforcing admission webhook resources", cp) + } + + log.Trace(logger, "ensuring admission webhook service", cp) + res, admissionWebhookService, err := r.ensureAdmissionWebhookService(ctx, logger, r.Client, cp) + if err != nil { + return "", res, fmt.Errorf("failed to ensure admission webhook service: %w", err) + } + if res != op.Noop { + if !webhookEnabled { + log.Debug(logger, "admission webhook service has been removed", cp) + } else { + log.Debug(logger, "admission webhook service has been created/updated", cp) + } + return "", res, nil // requeue will be triggered by the creation or update of the owned object + } + + log.Trace(logger, "ensuring admission webhook certificate", cp) + res, admissionWebhookCertificateSecret, err := r.ensureAdmissionWebhookCertificateSecret(ctx, logger, cp, admissionWebhookService) + if err != nil { + return "", res, err + } + if res != op.Noop { + if !webhookEnabled { + log.Debug(logger, "admission webhook service certificate has been removed", cp) + } else { + log.Debug(logger, "admission webhook service certificate has been created/updated", cp) + } + return "", res, nil // requeue will be triggered by the creation or update of the owned object + } + + log.Trace(logger, "ensuring admission webhook configuration", cp) + res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, admissionWebhookCertificateSecret, admissionWebhookService) + if err != nil { + return "", res, err + } + if res != op.Noop { + if !webhookEnabled { + log.Debug(logger, "ValidatingWebhookConfiguration has been removed", cp) + } else { + log.Debug(logger, "ValidatingWebhookConfiguration has been created/updated", cp) + } + } + if webhookEnabled { + return admissionWebhookCertificateSecret.Name, res, nil + } + return "", res, nil +} + +func isAdmissionWebhookEnabled(ctx context.Context, cl client.Client, logger logr.Logger, cp *operatorv1beta1.ControlPlane) bool { + if cp.Spec.Deployment.PodTemplateSpec == nil { + return false + } + + container := k8sutils.GetPodContainerByName(&cp.Spec.Deployment.PodTemplateSpec.Spec, consts.ControlPlaneControllerContainerName) + if container == nil { + return false + } + admissionWebhookListen, ok, err := k8sutils.GetEnvValueFromContainer(ctx, container, cp.Namespace, "CONTROLLER_ADMISSION_WEBHOOK_LISTEN", cl) + if err != nil { + log.Debug(logger, "unable to get CONTROLLER_ADMISSION_WEBHOOK_LISTEN env var", cp, "error", err) + return false + } + if !ok { + return false + } + // We don't validate the value of the env var here, just that it is set. + return len(admissionWebhookListen) > 0 && admissionWebhookListen != "off" +} diff --git a/controller/controlplane/controller_reconciler_utils.go b/controller/controlplane/controller_reconciler_utils.go index d4a9ae940..685a04d14 100644 --- a/controller/controlplane/controller_reconciler_utils.go +++ b/controller/controlplane/controller_reconciler_utils.go @@ -150,7 +150,7 @@ func (r *Reconciler) ensureDeployment( ctx context.Context, logger logr.Logger, params ensureDeploymentParams, -) (op.CreatedUpdatedOrNoop, *appsv1.Deployment, error) { +) (op.Result, *appsv1.Deployment, error) { dataplaneIsSet := params.ControlPlane.Spec.DataPlane != nil && *params.ControlPlane.Spec.DataPlane != "" deployments, err := k8sutils.ListDeploymentsForOwner(ctx, @@ -421,7 +421,7 @@ func (r *Reconciler) ensureAdminMTLSCertificateSecret( ctx context.Context, controlplane *operatorv1beta1.ControlPlane, ) ( - op.CreatedUpdatedOrNoop, + op.Result, *corev1.Secret, error, ) { @@ -452,10 +452,11 @@ func (r *Reconciler) ensureAdminMTLSCertificateSecret( // ControlPlane's admission webhook. func (r *Reconciler) ensureAdmissionWebhookCertificateSecret( ctx context.Context, + logger logr.Logger, cp *operatorv1beta1.ControlPlane, admissionWebhookService *corev1.Service, ) ( - op.CreatedUpdatedOrNoop, + op.Result, *corev1.Secret, error, ) { @@ -467,6 +468,24 @@ func (r *Reconciler) ensureAdmissionWebhookCertificateSecret( matchingLabels := client.MatchingLabels{ consts.SecretUsedByServiceLabel: consts.ControlPlaneServiceKindWebhook, } + if !isAdmissionWebhookEnabled(ctx, r.Client, logger, cp) { + labels := k8sresources.GetManagedLabelForOwner(cp) + labels[consts.SecretUsedByServiceLabel] = consts.ControlPlaneServiceKindWebhook + secrets, err := k8sutils.ListSecretsForOwner(ctx, r.Client, cp.GetUID(), matchingLabels) + if err != nil { + return op.Noop, nil, fmt.Errorf("failed listing Secrets for ControlPlane %s/: %w", client.ObjectKeyFromObject(cp), err) + } + for _, svc := range secrets { + if err := r.Client.Delete(ctx, &svc); err != nil { + return op.Noop, nil, fmt.Errorf("failed deleting ControlPlane admission webhook Secret %s: %w", svc.Name, err) + } + } + if len(secrets) == 0 { + return op.Noop, nil, nil + } + return op.Deleted, nil, nil + } + return secrets.EnsureCertificate(ctx, cp, fmt.Sprintf("%s.%s.svc", admissionWebhookService.Name, admissionWebhookService.Namespace), @@ -575,9 +594,10 @@ func (r *Reconciler) ensureOwnedValidatingWebhookConfigurationDeleted(ctx contex func (r *Reconciler) ensureAdmissionWebhookService( ctx context.Context, + logger logr.Logger, cl client.Client, controlPlane *operatorv1beta1.ControlPlane, -) (op.CreatedUpdatedOrNoop, *corev1.Service, error) { +) (op.Result, *corev1.Service, error) { matchingLabels := k8sresources.GetManagedLabelForOwner(controlPlane) matchingLabels[consts.ControlPlaneServiceLabel] = consts.ControlPlaneServiceKindWebhook @@ -592,6 +612,19 @@ func (r *Reconciler) ensureAdmissionWebhookService( return op.Noop, nil, fmt.Errorf("failed listing admission webhook Services for ControlPlane %s/%s: %w", controlPlane.Namespace, controlPlane.Name, err) } + if !isAdmissionWebhookEnabled(ctx, cl, logger, controlPlane) { + for _, svc := range services { + svc := svc + if err := cl.Delete(ctx, &svc); err != nil && !k8serrors.IsNotFound(err) { + return op.Noop, nil, fmt.Errorf("failed deleting ControlPlane admission webhook Service %s: %w", svc.Name, err) + } + } + if len(services) == 0 { + return op.Noop, nil, nil + } + return op.Deleted, nil, nil + } + count := len(services) if count > 1 { if err := k8sreduce.ReduceServices(ctx, cl, services); err != nil { @@ -639,8 +672,8 @@ func (r *Reconciler) ensureValidatingWebhookConfiguration( ctx context.Context, cp *operatorv1beta1.ControlPlane, certSecret *corev1.Secret, - webhookServiceName string, -) (op.CreatedUpdatedOrNoop, error) { + webhookService *corev1.Service, +) (op.Result, error) { logger := log.GetLogger(ctx, "controlplane.ensureValidatingWebhookConfiguration", r.DevelopmentMode) validatingWebhookConfigurations, err := k8sutils.ListValidatingWebhookConfigurationsForOwner( @@ -663,6 +696,18 @@ func (r *Reconciler) ensureValidatingWebhookConfiguration( return op.Noop, errors.New("number of validatingWebhookConfigurations reduced") } + if !isAdmissionWebhookEnabled(ctx, r.Client, logger, cp) { + for _, webhookConfiguration := range validatingWebhookConfigurations { + if err := r.Client.Delete(ctx, &webhookConfiguration); err != nil && !k8serrors.IsNotFound(err) { + return op.Noop, fmt.Errorf("failed deleting ControlPlane admission webhook ValidatingWebhookConfiguration %s: %w", webhookConfiguration.Name, err) + } + } + if len(validatingWebhookConfigurations) == 0 { + return op.Noop, nil + } + return op.Deleted, nil + } + cpContainer := k8sutils.GetPodContainerByName(&cp.Spec.Deployment.PodTemplateSpec.Spec, consts.ControlPlaneControllerContainerName) if cpContainer == nil { return op.Noop, errors.New("controller container not found") @@ -679,7 +724,7 @@ func (r *Reconciler) ensureValidatingWebhookConfiguration( admregv1.WebhookClientConfig{ Service: &admregv1.ServiceReference{ Namespace: cp.Namespace, - Name: webhookServiceName, + Name: webhookService.GetName(), Port: lo.ToPtr(int32(consts.ControlPlaneAdmissionWebhookListenPort)), }, CABundle: caBundle, diff --git a/controller/controlplane/controller_reconciler_utils_test.go b/controller/controlplane/controller_reconciler_utils_test.go index 4c68823d8..d3d06544f 100644 --- a/controller/controlplane/controller_reconciler_utils_test.go +++ b/controller/controlplane/controller_reconciler_utils_test.go @@ -19,7 +19,11 @@ import ( ) func Test_ensureValidatingWebhookConfiguration(t *testing.T) { - const webhookSvcName = "webhook-svc" + webhookSvc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-svc", + }, + } testCases := []struct { name string @@ -41,7 +45,20 @@ func Test_ensureValidatingWebhookConfiguration(t *testing.T) { PodTemplateSpec: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ - resources.GenerateControlPlaneContainer(consts.DefaultControlPlaneImage), + func() corev1.Container { + c := resources.GenerateControlPlaneContainer( + resources.GenerateContainerForControlPlaneParams{ + Image: consts.DefaultControlPlaneImage, + AdmissionWebhookCertSecretName: lo.ToPtr("cert-secret"), + }) + // Envs are set elsewhere so fill in the CONTROLLER_ADMISSION_WEBHOOK_LISTEN + // here so that the webhook is enabled. + c.Env = append(c.Env, corev1.EnvVar{ + Name: "CONTROLLER_ADMISSION_WEBHOOK_LISTEN", + Value: "0.0.0.0:8080", + }) + return c + }(), }, }, }, @@ -59,23 +76,23 @@ func Test_ensureValidatingWebhookConfiguration(t *testing.T) { certSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "cert-cecret", + Name: "cert-secret", }, Data: map[string][]byte{ "ca.crt": []byte("ca"), // dummy }, } - res, err := r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvcName) + res, err := r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvc) require.NoError(t, err) - require.Equal(t, res, op.Created) + require.Equal(t, op.Created, res) require.NoError(t, r.Client.List(ctx, &webhooks)) require.Len(t, webhooks.Items, 1) - res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvcName) + res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvc) require.NoError(t, err) - require.Equal(t, res, op.Noop) + require.Equal(t, op.Noop, res) }, }, { @@ -91,7 +108,20 @@ func Test_ensureValidatingWebhookConfiguration(t *testing.T) { PodTemplateSpec: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ - resources.GenerateControlPlaneContainer(consts.DefaultControlPlaneImage), + func() corev1.Container { + c := resources.GenerateControlPlaneContainer( + resources.GenerateContainerForControlPlaneParams{ + Image: consts.DefaultControlPlaneImage, + AdmissionWebhookCertSecretName: lo.ToPtr("cert-secret"), + }) + // Envs are set elsewhere so fill in the CONTROLLER_ADMISSION_WEBHOOK_LISTEN + // here so that the webhook is enabled. + c.Env = append(c.Env, corev1.EnvVar{ + Name: "CONTROLLER_ADMISSION_WEBHOOK_LISTEN", + Value: "0.0.0.0:8080", + }) + return c + }(), }, }, }, @@ -109,21 +139,21 @@ func Test_ensureValidatingWebhookConfiguration(t *testing.T) { certSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "cert-cecret", + Name: "cert-secret", }, Data: map[string][]byte{ "ca.crt": []byte("ca"), // dummy }, } - res, err := r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvcName) + res, err := r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvc) require.NoError(t, err) require.Equal(t, res, op.Created) require.NoError(t, r.Client.List(ctx, &webhooks)) require.Len(t, webhooks.Items, 1, "webhook configuration should be created") - res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvcName) + res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvc) require.NoError(t, err) require.Equal(t, res, op.Noop) @@ -135,7 +165,7 @@ func Test_ensureValidatingWebhookConfiguration(t *testing.T) { } t.Log("running ensureValidatingWebhookConfiguration to enforce ObjectMeta") - res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvcName) + res, err = r.ensureValidatingWebhookConfiguration(ctx, cp, certSecret, webhookSvc) require.NoError(t, err) require.Equal(t, res, op.Updated) diff --git a/controller/controlplane/controller_utils_test.go b/controller/controlplane/controller_utils_test.go index 04e4015b6..f6baf0f40 100644 --- a/controller/controlplane/controller_utils_test.go +++ b/controller/controlplane/controller_utils_test.go @@ -527,7 +527,6 @@ func TestSetControlPlaneDefaults(t *testing.T) { t.Run(tc.name, func(t *testing.T) { changed := controlplane.SetDefaults( tc.spec, - map[string]struct{}{}, controlplane.DefaultsArgs{ Namespace: tc.namespace, DataPlaneIngressServiceName: tc.dataplaneIngressServiceName, diff --git a/controller/dataplane/bluegreen_controller.go b/controller/dataplane/bluegreen_controller.go index 9eadfb74d..351ec9572 100644 --- a/controller/dataplane/bluegreen_controller.go +++ b/controller/dataplane/bluegreen_controller.go @@ -484,7 +484,7 @@ func (r *BlueGreenReconciler) ensureDeploymentForDataPlane( logger logr.Logger, dataplane *operatorv1beta1.DataPlane, certSecret *corev1.Secret, -) (*appsv1.Deployment, op.CreatedUpdatedOrNoop, error) { +) (*appsv1.Deployment, op.Result, error) { deploymentOpts := []k8sresources.DeploymentOpt{ labelSelectorFromDataPlaneRolloutStatusSelectorDeploymentOpt(dataplane), } @@ -747,7 +747,7 @@ func (r *BlueGreenReconciler) ensurePreviewAdminAPIService( ctx context.Context, logger logr.Logger, dataplane *operatorv1beta1.DataPlane, -) (op.CreatedUpdatedOrNoop, *corev1.Service, error) { +) (op.Result, *corev1.Service, error) { additionalServiceLabels := map[string]string{ consts.DataPlaneServiceStateLabel: consts.DataPlaneStateLabelValuePreview, } @@ -768,6 +768,7 @@ func (r *BlueGreenReconciler) ensurePreviewAdminAPIService( log.Debug(logger, "preview admin service modified", dataplane, "service", svc.Name, "reason", res) case op.Noop: log.Trace(logger, "no need for preview Admin API service update", dataplane) + case op.Deleted: } return res, svc, nil // dataplane admin service creation/update will trigger reconciliation } @@ -778,7 +779,7 @@ func (r *BlueGreenReconciler) ensurePreviewIngressService( ctx context.Context, logger logr.Logger, dataplane *operatorv1beta1.DataPlane, -) (op.CreatedUpdatedOrNoop, *corev1.Service, error) { +) (op.Result, *corev1.Service, error) { additionalServiceLabels := map[string]string{ consts.DataPlaneServiceStateLabel: consts.DataPlaneStateLabelValuePreview, } @@ -800,6 +801,7 @@ func (r *BlueGreenReconciler) ensurePreviewIngressService( log.Debug(logger, "preview ingress service modified", dataplane, "service", svc.Name, "reason", res) case op.Noop: log.Trace(logger, "no need for preview ingress service update", dataplane) + case op.Deleted: } return res, svc, nil diff --git a/controller/dataplane/bluegreen_controller_test.go b/controller/dataplane/bluegreen_controller_test.go index 4c0206a1c..60ba7c083 100644 --- a/controller/dataplane/bluegreen_controller_test.go +++ b/controller/dataplane/bluegreen_controller_test.go @@ -448,7 +448,7 @@ func TestEnsurePreviewIngressService(t *testing.T) { name string dataplane *operatorv1beta1.DataPlane existingServiceModifier func(*testing.T, context.Context, client.Client, *corev1.Service) - expectedCreatedOrUpdated op.CreatedUpdatedOrNoop + expectedCreatedOrUpdated op.Result expectedService *corev1.Service // expectedErrorMessage is empty if we expect no error, otherwise returned error must contain it. expectedErrorMessage string diff --git a/controller/dataplane/controller.go b/controller/dataplane/controller.go index 06ebb79ed..754798a86 100644 --- a/controller/dataplane/controller.go +++ b/controller/dataplane/controller.go @@ -108,6 +108,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu log.Debug(logger, "DataPlane admin service modified", dataplane, "service", dataplaneAdminService.Name, "reason", res) return ctrl.Result{}, nil // dataplane admin service creation/update will trigger reconciliation case op.Noop: + case op.Deleted: // This should not happen. } log.Trace(logger, "exposing DataPlane deployment via service", dataplane) diff --git a/controller/dataplane/owned_deployment.go b/controller/dataplane/owned_deployment.go index 4a3833980..b777b4682 100644 --- a/controller/dataplane/owned_deployment.go +++ b/controller/dataplane/owned_deployment.go @@ -90,7 +90,7 @@ func (d *DeploymentBuilder) BuildAndDeploy( ctx context.Context, dataplane *operatorv1beta1.DataPlane, developmentMode bool, -) (*appsv1.Deployment, op.CreatedUpdatedOrNoop, error) { +) (*appsv1.Deployment, op.Result, error) { // run any preparatory callbacks beforeDeploymentCallbacks := NewCallbackRunner(d.client) cbErrors := beforeDeploymentCallbacks.For(dataplane).Runs(d.beforeCallbacks).Do(ctx, nil) @@ -294,7 +294,7 @@ func reconcileDataPlaneDeployment( dataplane *operatorv1beta1.DataPlane, existing *appsv1.Deployment, desired *appsv1.Deployment, -) (res op.CreatedUpdatedOrNoop, deploy *appsv1.Deployment, err error) { +) (res op.Result, deploy *appsv1.Deployment, err error) { if existing != nil { var updated bool original := existing.DeepCopy() diff --git a/controller/dataplane/owned_resources.go b/controller/dataplane/owned_resources.go index aac8a359e..8b64678de 100644 --- a/controller/dataplane/owned_resources.go +++ b/controller/dataplane/owned_resources.go @@ -36,7 +36,7 @@ func ensureDataPlaneCertificate( dataplane *operatorv1beta1.DataPlane, clusterCASecretNN types.NamespacedName, adminServiceNN types.NamespacedName, -) (op.CreatedUpdatedOrNoop, *corev1.Secret, error) { +) (op.Result, *corev1.Secret, error) { usages := []certificatesv1.KeyUsage{ certificatesv1.UsageKeyEncipherment, certificatesv1.UsageDigitalSignature, certificatesv1.UsageServerAuth, @@ -57,7 +57,7 @@ func ensureHPAForDataPlane( log logr.Logger, dataplane *operatorv1beta1.DataPlane, deploymentName string, -) (res op.CreatedUpdatedOrNoop, hpa *autoscalingv2.HorizontalPodAutoscaler, err error) { +) (res op.Result, hpa *autoscalingv2.HorizontalPodAutoscaler, err error) { matchingLabels := k8sresources.GetManagedLabelForOwner(dataplane) hpas, err := k8sutils.ListHPAsForOwner( ctx, @@ -141,7 +141,7 @@ func ensureAdminServiceForDataPlane( dataPlane *operatorv1beta1.DataPlane, additionalServiceLabels client.MatchingLabels, opts ...k8sresources.ServiceOpt, -) (res op.CreatedUpdatedOrNoop, svc *corev1.Service, err error) { +) (res op.Result, svc *corev1.Service, err error) { // TODO: https://github.com/Kong/gateway-operator/issues/156. // Use only new labels after several minor version of soak time. @@ -252,7 +252,7 @@ func ensureIngressServiceForDataPlane( dataPlane *operatorv1beta1.DataPlane, additionalServiceLabels client.MatchingLabels, opts ...k8sresources.ServiceOpt, -) (op.CreatedUpdatedOrNoop, *corev1.Service, error) { +) (op.Result, *corev1.Service, error) { // TODO: https://github.com/Kong/gateway-operator/issues/156. // Use only new labels after several minor version of soak time. diff --git a/controller/dataplane/owned_resources_test.go b/controller/dataplane/owned_resources_test.go index ab57f2bdb..92df60fe1 100644 --- a/controller/dataplane/owned_resources_test.go +++ b/controller/dataplane/owned_resources_test.go @@ -28,7 +28,7 @@ func TestEnsureIngressServiceForDataPlane(t *testing.T) { dataplane *operatorv1beta1.DataPlane additionalLabels map[string]string existingServiceModifier func(*testing.T, context.Context, client.Client, *corev1.Service) - expectedCreatedOrUpdated op.CreatedUpdatedOrNoop + expectedCreatedOrUpdated op.Result expectedServiceType corev1.ServiceType expectedServicePorts []corev1.ServicePort expectedAnnotations map[string]string diff --git a/controller/gateway/controller_test.go b/controller/gateway/controller_test.go index e63781bf4..284a78675 100644 --- a/controller/gateway/controller_test.go +++ b/controller/gateway/controller_test.go @@ -320,7 +320,6 @@ func TestGatewayReconciler_Reconcile(t *testing.T) { controlPlane := gatewaySubResource.(*operatorv1beta1.ControlPlane) _ = controlplane.SetDefaults( &controlPlane.Spec.ControlPlaneOptions, - map[string]struct{}{}, controlplane.DefaultsArgs{ Namespace: "test-namespace", DataPlaneIngressServiceName: "test-ingress-service", diff --git a/controller/gateway/controller_watch.go b/controller/gateway/controller_watch.go index 975d42890..d123c2d73 100644 --- a/controller/gateway/controller_watch.go +++ b/controller/gateway/controller_watch.go @@ -322,7 +322,6 @@ func (r *Reconciler) setControlPlaneGatewayConfigDefaults(gateway *gwtypes.Gatew dataplaneAdminServiceName, controlPlaneName string, ) { - dontOverride := make(map[string]struct{}) if gatewayConfig.Spec.ControlPlaneOptions == nil { gatewayConfig.Spec.ControlPlaneOptions = new(operatorv1beta1.ControlPlaneOptions) } @@ -345,16 +344,17 @@ func (r *Reconciler) setControlPlaneGatewayConfigDefaults(gateway *gwtypes.Gatew // This change will not be saved in the API server (i.e. user applied resource // will not be changed) - which is the desired behavior - since the caller // only uses the changed GatewayConfiguration to generate ControlPlane resource. - container = lo.ToPtr[corev1.Container](resources.GenerateControlPlaneContainer(consts.DefaultControlPlaneImage)) + container = lo.ToPtr[corev1.Container](resources.GenerateControlPlaneContainer( + resources.GenerateContainerForControlPlaneParams{ + Image: consts.DefaultControlPlaneImage, + }, + )) controlPlanePodTemplateSpec.Spec.Containers = append(controlPlanePodTemplateSpec.Spec.Containers, *container) } - for _, env := range container.Env { - dontOverride[env.Name] = struct{}{} - } // an actual ControlPlane will have ObjectMeta populated with ownership information. this includes a stand-in to // satisfy the signature - _ = controlplane.SetDefaults(gatewayConfig.Spec.ControlPlaneOptions, dontOverride, + _ = controlplane.SetDefaults(gatewayConfig.Spec.ControlPlaneOptions, controlplane.DefaultsArgs{ Namespace: gateway.Namespace, DataPlaneIngressServiceName: dataplaneIngressServiceName, diff --git a/controller/pkg/controlplane/controlplane.go b/controller/pkg/controlplane/controlplane.go index 7934e7048..d7267fb61 100644 --- a/controller/pkg/controlplane/controlplane.go +++ b/controller/pkg/controlplane/controlplane.go @@ -35,7 +35,6 @@ type DefaultsArgs struct { // and returns true if env field is changed. func SetDefaults( spec *operatorv1beta1.ControlPlaneOptions, - dontOverride map[string]struct{}, args DefaultsArgs, ) bool { changed := false @@ -58,6 +57,10 @@ func SetDefaults( Name: consts.ControlPlaneControllerContainerName, } } + dontOverride := make(map[string]struct{}) + for _, envVar := range container.Env { + dontOverride[envVar.Name] = struct{}{} + } const podNamespaceEnvVarName = "POD_NAMESPACE" if !reflect.DeepEqual(envSourceMetadataNamespace, k8sutils.EnvVarSourceByName(container.Env, podNamespaceEnvVarName)) { diff --git a/controller/pkg/op/operation_result.go b/controller/pkg/op/operation_result.go index 78a7adcd3..16b9d78fb 100644 --- a/controller/pkg/op/operation_result.go +++ b/controller/pkg/op/operation_result.go @@ -1,16 +1,18 @@ package op -// CreatedUpdatedOrNoop represents a result of an operation that can either: +// Result represents a result of an operation that can either: // - create a resource // - update a resource // - do nothing -type CreatedUpdatedOrNoop string +type Result string const ( // Created indicates that an operation resulted in creation of a resource. - Created CreatedUpdatedOrNoop = "created" + Created Result = "created" // Updated indicates that an operation resulted in an update of a resource. - Updated CreatedUpdatedOrNoop = "updated" + Updated Result = "updated" + // Deleted indicates that an operation resulted in a delete of a resource. + Deleted Result = "deleted" // Noop indicated that an operation did not perform any actions. - Noop CreatedUpdatedOrNoop = "noop" + Noop Result = "noop" ) diff --git a/controller/pkg/patch/patch.go b/controller/pkg/patch/patch.go index ba848309f..99e6d2d36 100644 --- a/controller/pkg/patch/patch.go +++ b/controller/pkg/patch/patch.go @@ -34,7 +34,7 @@ func ApplyPatchIfNonEmpty[ oldExistingResource ResourceT, owner OwnerT, updated bool, -) (res op.CreatedUpdatedOrNoop, deploy ResourceT, err error) { +) (res op.Result, deploy ResourceT, err error) { kind := existingResource.GetObjectKind().GroupVersionKind().Kind if !updated { @@ -69,7 +69,7 @@ func ApplyGatewayStatusPatchIfNotEmpty(ctx context.Context, logger logr.Logger, existingGateway *gatewayv1.Gateway, oldExistingGateway *gatewayv1.Gateway, -) (res op.CreatedUpdatedOrNoop, err error) { +) (res op.Result, err error) { // Check if the patch to be applied is empty. patch := client.MergeFrom(oldExistingGateway) b, err := patch.Data(existingGateway) diff --git a/controller/pkg/patch/patch_test.go b/controller/pkg/patch/patch_test.go index bdbc50205..5c6143265 100644 --- a/controller/pkg/patch/patch_test.go +++ b/controller/pkg/patch/patch_test.go @@ -34,7 +34,7 @@ func TestApplyPatchIfNonEmpty(t *testing.T) { assertHPAFunc func(t *testing.T, hpa *autoscalingv2.HorizontalPodAutoscaler) updated bool wantErr bool - wantResult op.CreatedUpdatedOrNoop + wantResult op.Result }{ { name: "when no changes are needed no patch is being made", diff --git a/controller/pkg/secrets/cert.go b/controller/pkg/secrets/cert.go index 5a99a4db8..120aaf236 100644 --- a/controller/pkg/secrets/cert.go +++ b/controller/pkg/secrets/cert.go @@ -164,7 +164,7 @@ func EnsureCertificate[ usages []certificatesv1.KeyUsage, cl client.Client, additionalMatchingLabels client.MatchingLabels, -) (op.CreatedUpdatedOrNoop, *corev1.Secret, error) { +) (op.Result, *corev1.Secret, error) { setCALogger(ctrlruntimelog.Log) // TODO: https://github.com/Kong/gateway-operator-archive/pull/156. @@ -313,7 +313,7 @@ func generateTLSDataSecret( mtlsCASecret types.NamespacedName, usages []certificatesv1.KeyUsage, k8sClient client.Client, -) (op.CreatedUpdatedOrNoop, *corev1.Secret, error) { +) (op.Result, *corev1.Secret, error) { template := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: subject, diff --git a/controller/pkg/secrets/cert_test.go b/controller/pkg/secrets/cert_test.go index dc7925c2f..3bf68b956 100644 --- a/controller/pkg/secrets/cert_test.go +++ b/controller/pkg/secrets/cert_test.go @@ -230,7 +230,7 @@ func TestMaybeCreateCertificateSecret(t *testing.T) { subject string mtlsCASecretNN NN additionalMatchingLabels client.MatchingLabels - expectedResult op.CreatedUpdatedOrNoop + expectedResult op.Result expectedError error objectList client.ObjectList }{ diff --git a/hack/generators/go.mod b/hack/generators/go.mod index 3b8a1c86b..9415b8268 100644 --- a/hack/generators/go.mod +++ b/hack/generators/go.mod @@ -13,8 +13,8 @@ require ( github.com/kong/gateway-operator v0.0.0-00010101000000-000000000000 github.com/kong/semver/v4 v4.0.1 github.com/samber/lo v1.39.0 - k8s.io/api v0.30.1 - k8s.io/apimachinery v0.30.1 + k8s.io/api v0.30.2 + k8s.io/apimachinery v0.30.2 sigs.k8s.io/gateway-api v1.1.0 ) diff --git a/hack/generators/go.sum b/hack/generators/go.sum index 3c446cf93..17fda2dde 100644 --- a/hack/generators/go.sum +++ b/hack/generators/go.sum @@ -115,10 +115,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= -k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= -k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= -k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= diff --git a/pkg/consts/controlplane.go b/pkg/consts/controlplane.go index 4f131d1fc..af027068f 100644 --- a/pkg/consts/controlplane.go +++ b/pkg/consts/controlplane.go @@ -49,6 +49,8 @@ const ( // ----------------------------------------------------------------------------- const ( + // ControlPlaneAdmissionWebhookPortName is the name of the port on which the control plane admission webhook listens. + ControlPlaneAdmissionWebhookPortName = "webhook" // ControlPlaneAdmissionWebhookListenPort is the port on which the control plane admission webhook listens. ControlPlaneAdmissionWebhookListenPort = 8080 // ControlPlaneAdmissionWebhookEnvVarValue is the default value for the admission webhook env var. diff --git a/pkg/consts/dataplane.go b/pkg/consts/dataplane.go index 74bf388b0..e786c385f 100644 --- a/pkg/consts/dataplane.go +++ b/pkg/consts/dataplane.go @@ -87,7 +87,7 @@ const ( // DefaultDataPlaneTag is the base container image tag that can be used // by default for a DataPlane resource if all other attempts to dynamically // decide an image tag fail. - DefaultDataPlaneTag = "3.6" // renovate: datasource=docker depName=kong/kong-gateway + DefaultDataPlaneTag = "3.7" // renovate: datasource=docker depName=kong/kong-gateway // DefaultDataPlaneImage is the default container image that can be used if // all other attempts to dynamically decide the default image fail. diff --git a/pkg/utils/kubernetes/resources/deployments.go b/pkg/utils/kubernetes/resources/deployments.go index 79d14647d..8f9f3c9d0 100644 --- a/pkg/utils/kubernetes/resources/deployments.go +++ b/pkg/utils/kubernetes/resources/deployments.go @@ -92,15 +92,23 @@ func GenerateNewDeploymentForControlPlane(params GenerateNewDeploymentForControl SchedulerName: corev1.DefaultSchedulerName, Volumes: []corev1.Volume{ ClusterCertificateVolume(params.AdminMTLSCertSecretName), - controlPlaneAdmissionWebhookCertificateVolume(params.AdmissionWebhookCertSecretName), }, Containers: []corev1.Container{ - GenerateControlPlaneContainer(params.AdmissionWebhookCertSecretName), + GenerateControlPlaneContainer(GenerateContainerForControlPlaneParams{ + Image: params.ControlPlaneImage, + AdmissionWebhookCertSecretName: lo.ToPtr(params.AdmissionWebhookCertSecretName), + }), }, }, }, }, } + // Only add the admission webhook volume if the secret name is provided. + if params.AdmissionWebhookCertSecretName != "" { + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, + controlPlaneAdmissionWebhookCertificateVolume(params.AdmissionWebhookCertSecretName), + ) + } SetDefaultsPodTemplateSpec(&deployment.Spec.Template) LabelObjectAsControlPlaneManaged(deployment) @@ -121,11 +129,19 @@ func GenerateNewDeploymentForControlPlane(params GenerateNewDeploymentForControl return deployment, nil } +// GenerateContainerForControlPlaneParams is a parameter struct for GenerateControlPlaneContainer function. +type GenerateContainerForControlPlaneParams struct { + Image string + // AdmissionWebhookCertSecretName is the name of the Secret that holds the certificate for the admission webhook. + // If this is nil, the admission webhook will not be enabled. + AdmissionWebhookCertSecretName *string +} + // GenerateControlPlaneContainer generates a control plane container. -func GenerateControlPlaneContainer(image string) corev1.Container { - return corev1.Container{ +func GenerateControlPlaneContainer(params GenerateContainerForControlPlaneParams) corev1.Container { + c := corev1.Container{ Name: consts.ControlPlaneControllerContainerName, - Image: image, + Image: params.Image, ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePath: corev1.TerminationMessagePathDefault, TerminationMessagePolicy: corev1.TerminationMessageReadFile, @@ -135,11 +151,6 @@ func GenerateControlPlaneContainer(image string) corev1.Container { ReadOnly: true, MountPath: consts.ClusterCertificateVolumeMountPath, }, - { - Name: consts.ControlPlaneAdmissionWebhookVolumeName, - ReadOnly: true, - MountPath: consts.ControlPlaneAdmissionWebhookVolumeMountPath, - }, }, Ports: []corev1.ContainerPort{ { @@ -147,16 +158,25 @@ func GenerateControlPlaneContainer(image string) corev1.Container { ContainerPort: 10254, Protocol: corev1.ProtocolTCP, }, - { - Name: "webhook", - ContainerPort: consts.ControlPlaneAdmissionWebhookListenPort, - Protocol: corev1.ProtocolTCP, - }, }, LivenessProbe: GenerateControlPlaneProbe("/healthz", intstr.FromInt(10254)), ReadinessProbe: GenerateControlPlaneProbe("/readyz", intstr.FromInt(10254)), Resources: *DefaultControlPlaneResources(), } + // Only add the admission webhook volume mount and port if the secret name is provided. + if params.AdmissionWebhookCertSecretName != nil && *params.AdmissionWebhookCertSecretName != "" { + c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{ + Name: consts.ControlPlaneAdmissionWebhookVolumeName, + ReadOnly: true, + MountPath: consts.ControlPlaneAdmissionWebhookVolumeMountPath, + }) + c.Ports = append(c.Ports, corev1.ContainerPort{ + Name: consts.ControlPlaneAdmissionWebhookPortName, + ContainerPort: consts.ControlPlaneAdmissionWebhookListenPort, + Protocol: corev1.ProtocolTCP, + }) + } + return c } const ( diff --git a/pkg/utils/kubernetes/resources/deployments_test.go b/pkg/utils/kubernetes/resources/deployments_test.go index a12be1f8e..5647d5d79 100644 --- a/pkg/utils/kubernetes/resources/deployments_test.go +++ b/pkg/utils/kubernetes/resources/deployments_test.go @@ -3,11 +3,14 @@ package resources import ( "testing" + "github.com/samber/lo" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" operatorv1beta1 "github.com/kong/gateway-operator/api/v1beta1" "github.com/kong/gateway-operator/pkg/consts" @@ -274,3 +277,308 @@ func TestGenerateNewDeploymentForDataPlane(t *testing.T) { }) } } + +func TestGenerateNewDeploymentForControlPlane(t *testing.T) { + tests := []struct { + name string + generateControlPlaneArgs GenerateNewDeploymentForControlPlaneParams + expectedDeployment *appsv1.Deployment + }{ + { + name: "base case works", + generateControlPlaneArgs: GenerateNewDeploymentForControlPlaneParams{ + ControlPlane: &operatorv1beta1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "test-namespace", + UID: types.UID("1234-5678-9012"), + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway-operator.konghq.com/v1beta1", + Kind: "ControlPlane", + }, + }, + ControlPlaneImage: "kong/kubernetes-ingress-controller:3.1.5", + AdmissionWebhookCertSecretName: "admission-webhook-certificate", + AdminMTLSCertSecretName: "cluster-certificate-secret-name", + }, + expectedDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "controlplane-cp-1-", + Namespace: "test-namespace", + Labels: map[string]string{ + "app": "cp-1", + "gateway-operator.konghq.com/managed-by": "controlplane", + "konghq.com/gateway-operator": "controlplane", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "gateway-operator.konghq.com/v1beta1", + Kind: "ControlPlane", + Name: "cp-1", + UID: types.UID("1234-5678-9012"), + Controller: lo.ToPtr(true), + }, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: lo.ToPtr(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "cp-1", + }, + }, + RevisionHistoryLimit: lo.ToPtr(int32(10)), + ProgressDeadlineSeconds: lo.ToPtr(int32(600)), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.DeploymentStrategyType("RollingUpdate"), + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxUnavailable: lo.ToPtr(intstr.FromString("25%")), + MaxSurge: lo.ToPtr(intstr.FromString("25%")), + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "cp-1", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "cluster-certificate", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "cluster-certificate-secret-name", + DefaultMode: lo.ToPtr(int32(420)), + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "tls.crt", + }, + { + Key: "tls.key", + Path: "tls.key", + }, + { + Key: "ca.crt", + Path: "ca.crt", + }, + }, + }, + }, + }, + { + Name: "admission-webhook-certificate", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "admission-webhook-certificate", + DefaultMode: lo.ToPtr(int32(420)), + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "tls.crt", + }, + { + Key: "tls.key", + Path: "tls.key", + }, + }, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: consts.ControlPlaneControllerContainerName, + Image: "kong/kubernetes-ingress-controller:3.1.5", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("20Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("100Mi"), + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "health", + ContainerPort: 10254, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "webhook", + ContainerPort: 8080, + Protocol: corev1.ProtocolTCP, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "cluster-certificate", + MountPath: "/var/cluster-certificate", + ReadOnly: true, + }, + { + Name: "admission-webhook-certificate", + MountPath: "/admission-webhook", + ReadOnly: true, + }, + }, + LivenessProbe: GenerateControlPlaneProbe("/healthz", intstr.FromInt(10254)), + ReadinessProbe: GenerateControlPlaneProbe("/readyz", intstr.FromInt(10254)), + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + ImagePullPolicy: corev1.PullIfNotPresent, + }, + }, + SecurityContext: &corev1.PodSecurityContext{}, + RestartPolicy: corev1.RestartPolicyAlways, + DNSPolicy: corev1.DNSClusterFirst, + SchedulerName: corev1.DefaultSchedulerName, + TerminationGracePeriodSeconds: lo.ToPtr(int64(30)), + }, + }, + }, + }, + }, + { + name: "no webhook cert secret name specified doesn't the webhook volume, volume mount nor port", + generateControlPlaneArgs: GenerateNewDeploymentForControlPlaneParams{ + ControlPlane: &operatorv1beta1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "test-namespace", + UID: types.UID("1234-5678-9012"), + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway-operator.konghq.com/v1beta1", + Kind: "ControlPlane", + }, + }, + ControlPlaneImage: "kong/kubernetes-ingress-controller:3.1.5", + AdminMTLSCertSecretName: "cluster-certificate-secret-name", + }, + expectedDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "controlplane-cp-1-", + Namespace: "test-namespace", + Labels: map[string]string{ + "app": "cp-1", + "gateway-operator.konghq.com/managed-by": "controlplane", + "konghq.com/gateway-operator": "controlplane", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "gateway-operator.konghq.com/v1beta1", + Kind: "ControlPlane", + Name: "cp-1", + UID: types.UID("1234-5678-9012"), + Controller: lo.ToPtr(true), + }, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: lo.ToPtr(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "cp-1", + }, + }, + RevisionHistoryLimit: lo.ToPtr(int32(10)), + ProgressDeadlineSeconds: lo.ToPtr(int32(600)), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.DeploymentStrategyType("RollingUpdate"), + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxUnavailable: lo.ToPtr(intstr.FromString("25%")), + MaxSurge: lo.ToPtr(intstr.FromString("25%")), + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "cp-1", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "cluster-certificate", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "cluster-certificate-secret-name", + DefaultMode: lo.ToPtr(int32(420)), + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "tls.crt", + }, + { + Key: "tls.key", + Path: "tls.key", + }, + { + Key: "ca.crt", + Path: "ca.crt", + }, + }, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: consts.ControlPlaneControllerContainerName, + Image: "kong/kubernetes-ingress-controller:3.1.5", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("20Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("100Mi"), + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "health", + ContainerPort: 10254, + Protocol: corev1.ProtocolTCP, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "cluster-certificate", + MountPath: "/var/cluster-certificate", + ReadOnly: true, + }, + }, + LivenessProbe: GenerateControlPlaneProbe("/healthz", intstr.FromInt(10254)), + ReadinessProbe: GenerateControlPlaneProbe("/readyz", intstr.FromInt(10254)), + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + ImagePullPolicy: corev1.PullIfNotPresent, + }, + }, + SecurityContext: &corev1.PodSecurityContext{}, + RestartPolicy: corev1.RestartPolicyAlways, + DNSPolicy: corev1.DNSClusterFirst, + SchedulerName: corev1.DefaultSchedulerName, + TerminationGracePeriodSeconds: lo.ToPtr(int64(30)), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + deployment, err := GenerateNewDeploymentForControlPlane(tt.generateControlPlaneArgs) + require.NoError(t, err) + require.Equal(t, tt.expectedDeployment, deployment) + }) + } +} diff --git a/renovate.json b/renovate.json index d3987de90..7f9bd40f5 100644 --- a/renovate.json +++ b/renovate.json @@ -43,7 +43,7 @@ "^internal/versions/controlplane\\.go$" ], "matchStrings": [ - ".+\\s+=\\s+\"(?.+)\"\\s+//\\s+renovate:\\s+datasource=(?.*)\\s+depName=(?.+?)" + ".+\\s+=\\s+\"(?.+)\"\\s+//\\s+renovate:\\s+datasource=(?.*)\\s+depName=(?.+)" ] } ]