diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java index 21cbaf5447..cbbb428aa2 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java @@ -30,6 +30,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.quarkus.logging.Log; +import org.keycloak.common.util.CollectionUtil; import org.keycloak.operator.Config; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; @@ -152,6 +153,11 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu overlayTemplate.getSpec().getContainers().get(0).getImage() != null) { status.addWarningMessage("The image of the keycloak container cannot be modified using podTemplate"); } + + if (overlayTemplate.getSpec() != null && + CollectionUtil.isNotEmpty(overlayTemplate.getSpec().getImagePullSecrets())) { + status.addWarningMessage("The imagePullSecrets of the keycloak container cannot be modified using podTemplate"); + } } private void mergeMaps(Map map1, Map map2, Consumer> consumer) { @@ -523,6 +529,10 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu container.getArgs().add("--optimized"); } + if (CollectionUtil.isNotEmpty(keycloakCR.getSpec().getImagePullSecrets())) { + baseDeployment.getSpec().getTemplate().getSpec().setImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets()); + } + container.setImagePullPolicy(config.keycloak().imagePullPolicy()); container.setEnv(getEnvVars()); diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java index adc862da27..7c8be4c668 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java @@ -19,6 +19,7 @@ package org.keycloak.operator.crds.v2alpha1.deployment; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.kubernetes.api.model.LocalObjectReference; import org.keycloak.operator.Constants; import javax.validation.constraints.NotNull; @@ -30,6 +31,8 @@ public class KeycloakSpec { private int instances = 1; @JsonPropertyDescription("Custom Keycloak image to be used.") private String image; + @JsonPropertyDescription("Secret(s) that might be used when pulling an image from a private container image registry or repository.") + private List imagePullSecrets; @JsonPropertyDescription("Configuration of the Keycloak server.\n" + "expressed as a keys (reference: https://www.keycloak.org/server/all-config) and values that can be either direct values or references to secrets.") private List serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316 @@ -110,6 +113,14 @@ public class KeycloakSpec { this.image = image; } + public List getImagePullSecrets() { + return this.imagePullSecrets; + } + + public void setImagePullSecrets(List imagePullSecrets) { + this.imagePullSecrets = imagePullSecrets; + } + public List getServerConfiguration() { return serverConfiguration; } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java index 24d897a574..00ed76f035 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java @@ -18,6 +18,9 @@ package org.keycloak.operator.testsuite.integration; import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.LocalObjectReference; +import io.fabric8.kubernetes.api.model.LocalObjectReferenceBuilder; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecBuilder; @@ -38,6 +41,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -52,6 +56,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition; import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak; import static org.keycloak.operator.testsuite.utils.K8sUtils.getDefaultKeycloakDeployment; +import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile; import static org.keycloak.operator.testsuite.utils.K8sUtils.waitForKeycloakToBeReady; @QuarkusTest @@ -397,6 +402,37 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { } } + @Test + @EnabledIfSystemProperty(named = OPERATOR_CUSTOM_IMAGE, matches = ".+") + public void testCustomImageWithImagePullSecrets() { + String imagePullSecretName = "docker-regcred-custom-kc-imagepullsecret-01"; + String secretDescriptorFilename = "test-docker-registry-secret.yaml"; + + try { + var kc = getDefaultKeycloakDeployment(); + kc.getSpec().setImage(customImage); + + handleFakeImagePullSecretCreation(kc, secretDescriptorFilename); + + deployKeycloak(k8sclient, kc, true); + + var pods = k8sclient + .pods() + .inNamespace(namespace) + .withLabels(Constants.DEFAULT_LABELS) + .list() + .getItems(); + + assertThat(pods.get(0).getSpec().getContainers().get(0).getArgs()).containsExactly("start", "--optimized"); + assertThat(pods.get(0).getSpec().getImagePullSecrets().size()).isEqualTo(1); + assertThat(pods.get(0).getSpec().getImagePullSecrets().get(0).getName()).isEqualTo(imagePullSecretName); + + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + @Test public void testHttpRelativePathWithPlainValue() { try { @@ -498,4 +534,12 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { } } + private void handleFakeImagePullSecretCreation(Keycloak keycloakCR, + String secretDescriptorFilename) { + + Secret imagePullSecret = getResourceFromFile(secretDescriptorFilename, Secret.class); + k8sclient.secrets().inNamespace(namespace).createOrReplace(imagePullSecret); + LocalObjectReference localObjRefAsSecretTmp = new LocalObjectReferenceBuilder().withName(imagePullSecret.getMetadata().getName()).build(); + keycloakCR.getSpec().setImagePullSecrets(Collections.singletonList(localObjRefAsSecretTmp)); + } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java index 8eed3a2150..f657841451 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java @@ -17,7 +17,10 @@ package org.keycloak.operator.testsuite.integration; +import io.fabric8.kubernetes.api.model.LocalObjectReference; +import io.fabric8.kubernetes.api.model.LocalObjectReferenceBuilder; import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.Serialization; import io.quarkus.logging.Log; @@ -27,9 +30,12 @@ import org.junit.jupiter.api.Test; import org.keycloak.operator.testsuite.utils.CRAssert; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import java.util.Collections; + import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition.HAS_ERRORS; +import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile; @QuarkusTest public class PodTemplateTest extends BaseOperatorTest { @@ -181,4 +187,39 @@ public class PodTemplateTest extends BaseOperatorTest { }); } + @Test + public void testPodTemplateIncorrectImagePullSecretsConfig() { + String imagePullSecretName = "docker-regcred-custom-kc-imagepullsecret-01"; + String secretDescriptorFilename = "test-docker-registry-secret.yaml"; + + Secret imagePullSecret = getResourceFromFile(secretDescriptorFilename, Secret.class); + k8sclient.secrets().inNamespace(namespace).createOrReplace(imagePullSecret); + LocalObjectReference localObjRefAsSecretTmp = new LocalObjectReferenceBuilder().withName(imagePullSecret.getMetadata().getName()).build(); + + assertThat(localObjRefAsSecretTmp.getName()).isNotNull(); + assertThat(localObjRefAsSecretTmp.getName()).isEqualTo(imagePullSecretName); + + var podTemplate = new PodTemplateSpecBuilder() + .withNewSpec() + .addAllToImagePullSecrets(Collections.singletonList(localObjRefAsSecretTmp)) + .endSpec() + .build(); + + var plainKc = getEmptyPodTemplateKeycloak(); + plainKc.getSpec().getUnsupported().setPodTeplate(podTemplate); + + // Act + k8sclient.resource(plainKc).createOrReplace(); + + // Assert + Log.info("Getting status of Keycloak"); + Awaitility + .await() + .ignoreExceptions() + .atMost(3, MINUTES).untilAsserted(() -> { + CRAssert.assertKeycloakStatusCondition(getCrSelector().get(), HAS_ERRORS, false, "imagePullSecrets"); + CRAssert.assertKeycloakStatusCondition(getCrSelector().get(), HAS_ERRORS, false, "cannot be modified"); + }); + } + } diff --git a/operator/src/test/resources/test-docker-registry-secret.yaml b/operator/src/test/resources/test-docker-registry-secret.yaml new file mode 100644 index 0000000000..79649fb4b8 --- /dev/null +++ b/operator/src/test/resources/test-docker-registry-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +data: + .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJrZXljbG9hazR0ZXN0IiwicGFzc3dvcmQiOiJ2S2xRJWMyNDY5RUBMIiwiZW1haWwiOiJhbmFzY2ltZUByZWRoYXQuY29tIiwiYXV0aCI6ImEyVjVZMnh2WVdzMGRHVnpkRHAyUzJ4UkpXTXlORFk1UlVCTSJ9fX0= +metadata: + name: docker-regcred-custom-kc-imagepullsecret-01 +type: kubernetes.io/dockerconfigjson