diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index f81cc4b8e2..f5e0d8ba9c 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -45,9 +45,13 @@ public final class Constants { public static final String INIT_CONTAINER_EXTENSIONS_FOLDER = "/opt/extensions"; public static final String INIT_CONTAINER_EXTENSIONS_ENV_VAR = "KEYCLOAK_EXTENSIONS"; - public static final Integer KEYCLOAK_SERVICE_PORT = 8080; + public static final Integer KEYCLOAK_HTTP_PORT = 8080; + public static final Integer KEYCLOAK_HTTPS_PORT = 8443; public static final String KEYCLOAK_SERVICE_PROTOCOL = "TCP"; public static final String KEYCLOAK_SERVICE_SUFFIX = "-service"; public static final Integer KEYCLOAK_DISCOVERY_SERVICE_PORT = 7800; public static final String KEYCLOAK_DISCOVERY_SERVICE_SUFFIX = "-discovery"; + + public static final String INSECURE_DISABLE = "INSECURE-DISABLE"; + public static final String CERTIFICATES_FOLDER = "/mnt/certificates"; } diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java index b8b72d68fb..e823aeb46c 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java @@ -355,6 +355,81 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu } } + private void configureHostname(Deployment deployment) { + var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); + var hostname = this.keycloakCR.getSpec().getHostname(); + var envVars = kcContainer.getEnv(); + if (this.keycloakCR.getSpec().isHostnameDisabled()) { + var disableStrictHostname = List.of( + new EnvVarBuilder() + .withName("KC_HOSTNAME_STRICT") + .withValue("false") + .build(), + new EnvVarBuilder() + .withName("KC_HOSTNAME_STRICT_BACKCHANNEL") + .withValue("false") + .build()); + + envVars.addAll(disableStrictHostname); + } else { + var enabledStrictHostname = List.of( + new EnvVarBuilder() + .withName("KC_HOSTNAME") + .withValue(hostname) + .build()); + + envVars.addAll(enabledStrictHostname); + } + } + + private void configureTLS(Deployment deployment) { + var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); + var tlsSecret = this.keycloakCR.getSpec().getTlsSecret(); + var envVars = kcContainer.getEnv(); + if (this.keycloakCR.getSpec().isHttp()) { + var disableTls = List.of( + new EnvVarBuilder() + .withName("KC_HTTP_ENABLED") + .withValue("true") + .build()); + + envVars.addAll(disableTls); + + kcContainer.getReadinessProbe().getExec().setCommand( + List.of("curl", "--head", "--fail", "--silent", "http://127.0.0.1:" + Constants.KEYCLOAK_HTTP_PORT + "/health/ready")); + kcContainer.getLivenessProbe().getExec().setCommand( + List.of("curl", "--head", "--fail", "--silent", "http://127.0.0.1:" + Constants.KEYCLOAK_HTTP_PORT + "/health/live")); + } else { + var enabledTls = List.of( + new EnvVarBuilder() + .withName("KC_HTTPS_CERTIFICATE_FILE") + .withValue(Constants.CERTIFICATES_FOLDER + "/tls.crt") + .build(), + new EnvVarBuilder() + .withName("KC_HTTPS_CERTIFICATE_KEY_FILE") + .withValue(Constants.CERTIFICATES_FOLDER + "/tls.key") + .build()); + + envVars.addAll(enabledTls); + + var volume = new VolumeBuilder() + .withName("keycloak-tls-certificates") + .withNewSecret() + .withSecretName(tlsSecret) + .withOptional(false) + .endSecret() + .build(); + + var volumeMount = new VolumeMountBuilder() + .withName(volume.getName()) + .withMountPath(Constants.CERTIFICATES_FOLDER) + .build(); + + deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume); + kcContainer.getVolumeMounts().add(volumeMount); + } + } + private Deployment createBaseDeployment() { var is = this.getClass().getResourceAsStream("/base-keycloak-deployment.yaml"); Deployment baseDeployment = Serialization.unmarshal(is, Deployment.class); @@ -371,6 +446,8 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu container.setEnv(getEnvVars()); + configureHostname(baseDeployment); + configureTLS(baseDeployment); addInitContainer(baseDeployment, keycloakCR.getSpec().getExtensions()); mergePodTemplate(baseDeployment.getSpec().getTemplate()); diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java index 726c344e0b..1476c6425b 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java @@ -83,7 +83,7 @@ public class KeycloakRealmImportJob extends OperatorManagedResource { .get(); } - private Job buildJob(Container keycloakContainer, Volume secretVolume) { + private Job buildJob(Container keycloakContainer, List volumes) { return new JobBuilder() .withNewMetadata() .withName(getName()) @@ -93,7 +93,7 @@ public class KeycloakRealmImportJob extends OperatorManagedResource { .withNewTemplate() .withNewSpec() .withContainers(keycloakContainer) - .addToVolumes(secretVolume) + .withVolumes(volumes) .withRestartPolicy("Never") .endSpec() .endTemplate() @@ -112,8 +112,10 @@ public class KeycloakRealmImportJob extends OperatorManagedResource { private Job createImportJob() { var keycloakContainer = buildKeycloakJobContainer(); - var secretVolume = buildSecretVolume(); - var importJob = buildJob(keycloakContainer, secretVolume); + + var volumes = this.existingDeployment.getSpec().getTemplate().getSpec().getVolumes(); + volumes.add(buildSecretVolume()); + var importJob = buildJob(keycloakContainer, volumes); return importJob; } @@ -142,14 +144,13 @@ public class KeycloakRealmImportJob extends OperatorManagedResource { .setCommand(command); keycloakContainer .setArgs(commandArgs); - var volumeMounts = List.of( - new VolumeMountBuilder() - .withName(volumeName) - .withReadOnly(true) - .withMountPath(importMntPath) - .build()); + var volumeMount = new VolumeMountBuilder() + .withName(volumeName) + .withReadOnly(true) + .withMountPath(importMntPath) + .build(); - keycloakContainer.setVolumeMounts(volumeMounts); + keycloakContainer.getVolumeMounts().add(volumeMount); // Disable probes since we are not really starting the server keycloakContainer.setReadinessProbe(null); diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakService.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakService.java index 81ff66183c..4710c9dd11 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakService.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakService.java @@ -17,7 +17,6 @@ package org.keycloak.operator.v2alpha1; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceSpec; @@ -34,16 +33,19 @@ import java.util.Optional; public class KeycloakService extends OperatorManagedResource implements StatusUpdater { private Service existingService; + private final Keycloak keycloak; public KeycloakService(KubernetesClient client, Keycloak keycloakCR) { super(client, keycloakCR); + this.keycloak = keycloakCR; this.existingService = fetchExistingService(); } private ServiceSpec getServiceSpec() { + var port = (this.keycloak.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT; return new ServiceSpecBuilder() .addNewPort() - .withPort(Constants.KEYCLOAK_SERVICE_PORT) + .withPort(port) .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) .endPort() .withSelector(Constants.DEFAULT_LABELS) diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java index 0583ad209b..ea087a30ca 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java @@ -18,8 +18,10 @@ package org.keycloak.operator.v2alpha1.crds; import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import org.keycloak.operator.Constants; import org.keycloak.operator.v2alpha1.crds.keycloakspec.Unsupported; +import javax.validation.constraints.NotNull; import java.util.List; import java.util.Map; @@ -28,6 +30,16 @@ public class KeycloakSpec { private int instances = 1; private String image; private Map serverConfiguration; + + @NotNull + @JsonPropertyDescription("Hostname for the Keycloak server.\n" + + "The special value `" + Constants.INSECURE_DISABLE + "` disables the hostname strict resolution.") + private String hostname; + @NotNull + @JsonPropertyDescription("A secret containing the TLS configuration for HTTPS. Reference: https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets.\n" + + "The special value `" + Constants.INSECURE_DISABLE + "` disables https.") + private String tlsSecret; + @JsonPropertyDescription("List of URLs to download Keycloak extensions.") private List extensions; @JsonPropertyDescription( @@ -35,6 +47,30 @@ public class KeycloakSpec { "Use at your own risk and open an issue with your use-case if you don't find an alternative way.") private Unsupported unsupported; + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public boolean isHostnameDisabled() { + return this.hostname.equals(Constants.INSECURE_DISABLE); + } + + public String getTlsSecret() { + return tlsSecret; + } + + public void setTlsSecret(String tlsSecret) { + this.tlsSecret = tlsSecret; + } + + public boolean isHttp() { + return this.tlsSecret.equals(Constants.INSECURE_DISABLE); + } + public List getExtensions() { return extensions; } diff --git a/operator/src/main/resources/base-keycloak-deployment.yaml b/operator/src/main/resources/base-keycloak-deployment.yaml index 2f6a62646e..f8d92db456 100644 --- a/operator/src/main/resources/base-keycloak-deployment.yaml +++ b/operator/src/main/resources/base-keycloak-deployment.yaml @@ -18,7 +18,8 @@ spec: spec: containers: - args: - - start-dev + - start + - --auto-build imagePullPolicy: Always name: keycloak ports: @@ -27,16 +28,26 @@ spec: - containerPort: 8080 protocol: TCP livenessProbe: - httpGet: - path: /health/live - port: 8080 + exec: + command: + - curl + - --insecure + - --head + - --fail + - --silent + - https://127.0.0.1:8443/health/live initialDelaySeconds: 20 periodSeconds: 2 failureThreshold: 100 readinessProbe: - httpGet: - path: /health/ready - port: 8080 + exec: + command: + - curl + - --insecure + - --head + - --fail + - --silent + - https://127.0.0.1:8443/health/ready initialDelaySeconds: 20 periodSeconds: 2 failureThreshold: 200 diff --git a/operator/src/main/resources/example-keycloak.yml b/operator/src/main/resources/example-keycloak.yml index 9c98eddeba..afc69ef7de 100644 --- a/operator/src/main/resources/example-keycloak.yml +++ b/operator/src/main/resources/example-keycloak.yml @@ -11,6 +11,8 @@ spec: # KC_DB_PASSWORD: ${secret:keycloak-db-secret:password} KC_DB_USERNAME: postgres KC_DB_PASSWORD: testpassword + hostname: example.com + tlsSecret: example-tls-secret --- apiVersion: v1 kind: Secret @@ -19,4 +21,13 @@ metadata: data: username: cG9zdGdyZXM= # postgres password: dGVzdHBhc3N3b3Jk # testpassword -type: Opaque \ No newline at end of file +type: Opaque +--- +apiVersion: v1 +kind: Secret +metadata: + name: example-tls-secret +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVmekNDQXVlZ0F3SUJBZ0lSQUlVenBxa1FoaTNKclZBcmxVNVRhVTB3RFFZSktvWklodmNOQVFFTEJRQXcKZ1lreEhqQWNCZ05WQkFvVEZXMXJZMlZ5ZENCa1pYWmxiRzl3YldWdWRDQkRRVEV2TUMwR0ExVUVDd3dtWVhCbApjblZtWm05QVlYQmxjblZtWm04dGJXRmpJQ2hCYm1SeVpXRWdVR1Z5ZFdabWJ5a3hOakEwQmdOVkJBTU1MVzFyClkyVnlkQ0JoY0dWeWRXWm1iMEJoY0dWeWRXWm1ieTF0WVdNZ0tFRnVaSEpsWVNCUVpYSjFabVp2S1RBZUZ3MHkKTWpBek1ETXhNVEExTlRWYUZ3MHlOREEyTURNeE1EQTFOVFZhTUZveEp6QWxCZ05WQkFvVEhtMXJZMlZ5ZENCawpaWFpsYkc5d2JXVnVkQ0JqWlhKMGFXWnBZMkYwWlRFdk1DMEdBMVVFQ3d3bVlYQmxjblZtWm05QVlYQmxjblZtClptOHRiV0ZqSUNoQmJtUnlaV0VnVUdWeWRXWm1ieWt3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXcKZ2dFS0FvSUJBUUN5MjljQ0JrSzZNWERNbWZONy9TVmdiNXR2WXFWc01LVjhjaEwvTE5UcXVkdVA0QVBZeEtzMApQWnZBd0RRa3lGUXRxQlVvTXBhelBCaUpyREZ2eHc2VDZaeGVUOXlobCtvNWxhVmdseUdUMC9TcTBjTkg3UkZaCk5KeXpEZDdhREVjc2E0cmZmVEJPbk9UZjZ3QzhuSkNobTl4Mm9FWlU0UHRIb2tKZzcrVlFXYUdVRHg3Wm5YSlgKUXQ5SXFSb1dQWW1BWnNQc1FUNzdPeWkzUGZSa2NqZ1FTWEJsWVhNWXFZOWxMZTZpR2NldnNkdGhyOEdOZFF4dQpJV3RBOTYwdkgzSFpwRmgyRXRJbnVEOTdlWjU4STB4WXZuU2xSZGlXV1BPSTNwWDFvR0xyWDZjWGl1RlRDNUg3ClB3NnVSZUdVZ2tvR2tXS1pSU3RZdGp1dENuZHEvZ2JuQWdNQkFBR2pnWTh3Z1l3d0RnWURWUjBQQVFIL0JBUUQKQWdXZ01CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUJNQjhHQTFVZEl3UVlNQmFBRkg2Qmh5V21zVEpwMTdqSApVLzlKaDI1MUdhMTFNRVFHQTFVZEVRUTlNRHVDQzJWNFlXMXdiR1V1WTI5dGdnbHRlV0Z3Y0M1a1pYYUNDV3h2ClkyRnNhRzl6ZEljRWZ3QUFBWWNRQUFBQUFBQUFBQUFBQUFBQUFBQUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0MKQVlFQWYrazRMQW11YjlLKzM3RWo5M3RwYXhZdER2cUl4d1VpVkRHUyt6TElrd296akkyaHVTYko2N0lsdVJZaQp0SjVUU3hlM1hMTTNJM1NQU2tKNUxpY0JLRjJDRW1tdDBKRnk2WERxeU80L3NncFVDWVh6V3J1ZWU5VWM4VkhNCnljL3ZLclN3bTVDek82alIyZk0xajdCUWVJdHh6Qk1rTlJYZUUxSUVJWGtYMUFFUGRYaFBHZXFya1NqYzdGbjkKSkIzeGIvN0xvdTNxSFlBV2xyeThicWd2Z0pjZFlVWE9RWlVZSXE0ekd4bkNZRFRTblRuTG8vbW5YQ0h6MHZXRApldlpRQzhsL2t2TWRNb1RNSUxWamxObFgyeTNyekw2ak1QZTIxcGpSdFd3K0R6S1E1dkdZemMxL1hFbXJRaVJVCmxlRWE4cVp4QVkySXptMW9hTWdNa0cwZklKRkEyZk9DSGVWTnJOek93S1ZjaXFGVHpUanpZMW9HZDd5bncrQ28KaUF1Tm03TERxdzczakJYMVBBK1ZYM0pnRTVlODVnQ0FVU0UzK0Y3Z1RGb1hBS1M3T255Mk9mS0xSREw3U0NPWgp1THlub1NVeTUrcnJlUjBJNzRwTXVhRm9hUHo5U2lCNzVCNnZ4eGZWV0xLN0g3T1ZxV1YyR0Qra3dxSW1hOUVJClVmV2IKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ3kyOWNDQmtLNk1YRE0KbWZONy9TVmdiNXR2WXFWc01LVjhjaEwvTE5UcXVkdVA0QVBZeEtzMFBadkF3RFFreUZRdHFCVW9NcGF6UEJpSgpyREZ2eHc2VDZaeGVUOXlobCtvNWxhVmdseUdUMC9TcTBjTkg3UkZaTkp5ekRkN2FERWNzYTRyZmZUQk9uT1RmCjZ3QzhuSkNobTl4Mm9FWlU0UHRIb2tKZzcrVlFXYUdVRHg3Wm5YSlhRdDlJcVJvV1BZbUFac1BzUVQ3N095aTMKUGZSa2NqZ1FTWEJsWVhNWXFZOWxMZTZpR2NldnNkdGhyOEdOZFF4dUlXdEE5NjB2SDNIWnBGaDJFdEludUQ5NwplWjU4STB4WXZuU2xSZGlXV1BPSTNwWDFvR0xyWDZjWGl1RlRDNUg3UHc2dVJlR1Vna29Ha1dLWlJTdFl0anV0CkNuZHEvZ2JuQWdNQkFBRUNnZ0VBWEtWSlV2QWhRa2IzMGROdzd1bXFvYkJPQ0QxRnlLdk9ISThPVGdWUDZLSUwKSEJTQ2laY2R3M3FpSWc2dE05eGMxaVY1aUEva1JjVThSSnZnSTdFdFdPcXFKNlFnZWNleCtOQU9FT0ZYOERYYgpSMXhPVmdSemR3eXNtb2IxeDJhU3UyeWRTN1NTQURaK3k0bjBJTDdNb0JtVzhnK0ZQdFFtOU8wVWl4ZllaV3lhCmVleHFOS0xLVS9neG5iZXIvQy9kVWpPS3dndmpDRHkvZjhGQ1BNcDBEZzFLdU1Uc2J5ZjRyczQvM1JkUDBtK08KdXZhTTJQaEJsNEJJQVg2NXRIc1p6TGRtZWhOdzd1RGR1eGhBenVwVkR6YlhKcGQ5cEZaWE83QzlGWXhVNFpuSgpHbnliWktQcDlrL28yVkw2OWR1d0NSdkYySlVWdEZQZ2pibG80R2o4Y1FLQmdRRFc3NXVhdGtHUmNHR1Y3QWE3CktWcXVwWXFoazlxOERvaG9CZ2xvZzZMb3REMzFxbks2b1hwM2UweS9mZElMaEFERGdCaStEYW05aGFicEV4Q3YKK29TcnVNbFJLM1EyN1ZzbFd6WVQ2K0JyZDRNS3RrNjN6TG1YS25iTWhsOE9TQ1FERmdrUXQrWExGak1FUmNmawpvb2JWem1qajdrWnh5c1hoV2xsdlFTaXkyUUtCZ1FEVkI3ZG9oNDcwZ3I2VjBvbko4VzlDazF6MTBWMEtCQ0ZjCmFkd3Z4UjBKdUhsc2ZmVVJsaU1zQ3VzQlYzWDJpb3liblNxeG14SGQ0Qm5zeWx4bFlLdEpkM2pQbE05bnVoajAKbWZwMzFIcEN6aWRZRUs5Q1RVVFBTZE5tcUlFdHJqTkppano0OHcxNWlTVFA2c2c4ZXhWVUdtTVkrUDVyeDM4SQpXSkxzU3VqdnZ3S0JnUUNrTW5QN0l4VEFHTXhVRGZXdWNZODNNSHZScC9SSUNnb20vY1dlTkVIMTZBd1ZhdHN1CnZFR2ttV3N1TnQ2SnNaUXJ4ZVlnK3FzYmY4amM4WldqK292ejY3elA1NVJtaWJsQnRvWi9mWWo2VUZpcGpGQmkKbFdHS25BUVpodVdETVpWaFRpb3F2WEl0VFk0M3kxOUR5TzJjMUl6STQ3U3BKYkU1MFIzVm9qK0hNUUtCZ0VkZwpESDJEWGN4aXVnUnN4Q25iTU5IM21kL3F3K2VGTnNCRjM3WkpyczhBOWYzNXZkQ2tveWd3aUVpc3l5Tk5qSXJlCi85ejkvZUIvSTNDSTVLZzYyV2tHRkg1SWQ2MWpWdFV0ZWhRSUp1YVhOK3R6dTZUVlNzYkJENG1IejdCRWUzNmEKU0krSXIrMFduRFRsankxa2QrTHo3RndEb1FydmpvcDNVdExFem9MMUFvR0JBTTcvWVRNWSszV1NDeENPL3NIWAo3OGZDeHhBRHFMVWMxVURYdGMzcFhKQnorL3hJeUx1Q3JQYnlsUC82L21yRjN4SENTbGg3bi9mcFovV1dRMzIxCjNyZnR5Y2czWWVzalZxdjBaZmJVb01OdFE5cGYrcFpQMGpWVEZXMlF3YTZWYURrcGdTQnB4QzlvWXlMWTRldGMKajBkWm9NeTVMYXNKcm5jUjhlTVc4NHlnCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K +type: kubernetes.io/tls diff --git a/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java b/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java index bcc38eae45..d475386c77 100644 --- a/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java +++ b/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java @@ -181,7 +181,7 @@ public abstract class ClusterOperatorTest { private static void setDefaultAwaitilityTimings() { Awaitility.setDefaultPollInterval(Duration.ofSeconds(1)); - Awaitility.setDefaultTimeout(Duration.ofSeconds(240)); + Awaitility.setDefaultTimeout(Duration.ofSeconds(360)); } @AfterEach diff --git a/operator/src/test/java/org/keycloak/operator/ClusteringE2EIT.java b/operator/src/test/java/org/keycloak/operator/ClusteringE2EIT.java index 65220f55d1..66816e93f3 100644 --- a/operator/src/test/java/org/keycloak/operator/ClusteringE2EIT.java +++ b/operator/src/test/java/org/keycloak/operator/ClusteringE2EIT.java @@ -15,7 +15,6 @@ import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport; import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition; import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition; -import java.io.IOException; import java.time.Duration; import static java.util.concurrent.TimeUnit.MINUTES; @@ -69,7 +68,7 @@ public class ClusteringE2EIT extends ClusterOperatorTest { // get the service var service = new KeycloakService(k8sclient, kc); - String url = "http://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_SERVICE_PORT; + String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT; Awaitility.await().atMost(5, MINUTES).untilAsserted(() -> { Log.info("Starting curl Pod to test if the realm is available"); @@ -98,6 +97,7 @@ public class ClusteringE2EIT extends ClusterOperatorTest { .resources(Keycloak.class) .inNamespace(kc.getMetadata().getNamespace()) .withName(kc.getMetadata().getName()); + K8sUtils.deployKeycloak(k8sclient, kc, false); var targetInstances = 3; kc.getSpec().setInstances(targetInstances); k8sclient.resources(Keycloak.class).inNamespace(namespace).createOrReplace(kc); @@ -136,14 +136,15 @@ public class ClusteringE2EIT extends ClusterOperatorTest { .pods() .inNamespace(namespace) .withName(pod.getMetadata().getName()) - .portForward(8080, 8080)) { + .portForward(8443, 8443)) { token = (token != null) ? token : RestAssured.given() + .relaxedHTTPSValidation() .param("grant_type" , "password") .param("client_id", "token-test-client") .param("username", "test") .param("password", "test") - .post("http://localhost:" + portForward.getLocalPort() + "/realms/token-test/protocol/openid-connect/token") + .post("https://localhost:" + portForward.getLocalPort() + "/realms/token-test/protocol/openid-connect/token") .body() .jsonPath() .getString("access_token"); @@ -151,8 +152,9 @@ public class ClusteringE2EIT extends ClusterOperatorTest { Log.info("Using token:" + token); var username = RestAssured.given() + .relaxedHTTPSValidation() .header("Authorization", "Bearer " + token) - .get("http://localhost:" + portForward.getLocalPort() + "/realms/token-test/protocol/openid-connect/userinfo") + .get("https://localhost:" + portForward.getLocalPort() + "/realms/token-test/protocol/openid-connect/userinfo") .body() .jsonPath() .getString("preferred_username"); @@ -174,23 +176,22 @@ public class ClusteringE2EIT extends ClusterOperatorTest { for (int i = 0; i < (targetInstances * 2); i++) { if (token2 == null) { - var tokenUrl = "http://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_SERVICE_PORT + "/realms/token-test/protocol/openid-connect/token"; + var tokenUrl = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT + "/realms/token-test/protocol/openid-connect/token"; Log.info("Checking url: " + tokenUrl); - var tokenOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "-s", "--data", "grant_type=password&client_id=token-test-client&username=test&password=test", tokenUrl); + var tokenOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-s", "--data", "grant_type=password&client_id=token-test-client&username=test&password=test", tokenUrl); Log.info("Curl Output with token: " + tokenOutput); JsonNode tokenAnswer = Serialization.jsonMapper().readTree(tokenOutput); assertThat(tokenAnswer.hasNonNull("access_token")).isTrue(); token2 = tokenAnswer.get("access_token").asText(); } - String url = "http://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_SERVICE_PORT + "/realms/token-test/protocol/openid-connect/userinfo"; + String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT + "/realms/token-test/protocol/openid-connect/userinfo"; Log.info("Checking url: " + url); - var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "-s", "-H", "Authorization: Bearer " + token2, url); + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-s", "-H", "Authorization: Bearer " + token2, url); Log.info("Curl Output on access attempt: " + curlOutput); - JsonNode answer = Serialization.jsonMapper().readTree(curlOutput); assertThat(answer.hasNonNull("preferred_username")).isTrue(); assertThat(answer.get("preferred_username").asText()).isEqualTo("test"); diff --git a/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java b/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java index 34a1cbcdc8..77ab61cb2c 100644 --- a/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java +++ b/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java @@ -6,6 +6,8 @@ import io.quarkus.logging.Log; import io.quarkus.test.junit.QuarkusTest; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.keycloak.operator.utils.K8sUtils; +import org.keycloak.operator.v2alpha1.KeycloakService; import org.keycloak.operator.v2alpha1.crds.Keycloak; import java.time.Duration; @@ -14,6 +16,7 @@ import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.keycloak.operator.Constants.DEFAULT_LABELS; import static org.keycloak.operator.utils.K8sUtils.deployKeycloak; @@ -145,4 +148,102 @@ public class KeycloakDeploymentE2EIT extends ClusterOperatorTest { } } + @Test + public void testTlsUsesCorrectSecret() { + try { + var kc = getDefaultKeycloakDeployment(); + deployKeycloak(k8sclient, kc, true); + + var service = new KeycloakService(k8sclient, kc); + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT; + Log.info("Checking url: " + url); + + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-s", "-v", url); + Log.info("Curl Output: " + curlOutput); + + assertTrue(curlOutput.contains("issuer: O=mkcert development CA; OU=aperuffo@aperuffo-mac (Andrea Peruffo); CN=mkcert aperuffo@aperuffo-mac (Andrea Peruffo)")); + }); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + + @Test + public void testTlsDisabled() { + try { + var kc = getDefaultKeycloakDeployment(); + kc.getSpec().setTlsSecret(Constants.INSECURE_DISABLE); + deployKeycloak(k8sclient, kc, true); + + var service = new KeycloakService(k8sclient, kc); + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + String url = "http://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTP_PORT; + Log.info("Checking url: " + url); + + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, url); + Log.info("Curl Output: " + curlOutput); + + assertEquals("200", curlOutput); + }); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + + @Test + public void testHostnameStrict() { + try { + var kc = getDefaultKeycloakDeployment(); + deployKeycloak(k8sclient, kc, true); + + var service = new KeycloakService(k8sclient, kc); + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT; + Log.info("Checking url: " + url); + + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-H", "Host: foo.bar", url); + Log.info("Curl Output: " + curlOutput); + + assertTrue(curlOutput.contains("")); + }); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + + @Test + public void testHostnameStrictDisabled() { + try { + var kc = getDefaultKeycloakDeployment(); + kc.getSpec().setHostname(Constants.INSECURE_DISABLE); + deployKeycloak(k8sclient, kc, true); + + var service = new KeycloakService(k8sclient, kc); + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT; + Log.info("Checking url: " + url); + + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-H", "Host: foo.bar", url); + Log.info("Curl Output: " + curlOutput); + + assertTrue(curlOutput.contains("")); + }); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + } diff --git a/operator/src/test/java/org/keycloak/operator/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/PodTemplateTest.java index 9a8e1c04a6..de9ef58dd0 100644 --- a/operator/src/test/java/org/keycloak/operator/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/PodTemplateTest.java @@ -43,6 +43,8 @@ public class PodTemplateTest { var kc = new Keycloak(); var spec = new KeycloakSpec(); spec.setUnsupported(new Unsupported(podTemplate)); + spec.setHostname("example.com"); + spec.setTlsSecret("example-tls-secret"); kc.setSpec(spec); var deployment = new KeycloakDeployment(null, config, kc, new Deployment()); return (Deployment) deployment.getReconciledResource().get(); @@ -98,7 +100,7 @@ public class PodTemplateTest { var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate(); // Assert - assertEquals(volumeName, podTemplate.getSpec().getVolumes().get(0).getName()); + assertEquals(volumeName, podTemplate.getSpec().getVolumes().get(1).getName()); } @Test @@ -121,8 +123,8 @@ public class PodTemplateTest { var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate(); // Assert - assertEquals(volumeMountName, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(0).getName()); - assertEquals(volumeMountPath, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(0).getMountPath()); + assertEquals(volumeMountName, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(1).getName()); + assertEquals(volumeMountPath, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(1).getMountPath()); } @Test diff --git a/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java b/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java index 0e03a2bae7..bb1e07a150 100644 --- a/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java +++ b/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java @@ -11,7 +11,7 @@ import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.keycloak.operator.Constants.KEYCLOAK_SERVICE_PORT; +import static org.keycloak.operator.Constants.KEYCLOAK_HTTPS_PORT; import static org.keycloak.operator.utils.K8sUtils.getDefaultKeycloakDeployment; import static org.keycloak.operator.utils.K8sUtils.inClusterCurl; import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.DONE; @@ -55,9 +55,9 @@ public class RealmImportE2EIT extends ClusterOperatorTest { }); var service = new KeycloakService(k8sclient, getDefaultKeycloakDeployment()); String url = - "http://" + service.getName() + "." + namespace + ":" + KEYCLOAK_SERVICE_PORT + "/realms/count0"; + "https://" + service.getName() + "." + namespace + ":" + KEYCLOAK_HTTPS_PORT + "/realms/count0"; - Awaitility.await().atMost(5, MINUTES).untilAsserted(() -> { + Awaitility.await().atMost(10, MINUTES).untilAsserted(() -> { Log.info("Starting curl Pod to test if the realm is available"); Log.info("Url: '" + url + "'"); String curlOutput = inClusterCurl(k8sclient, namespace, url); diff --git a/operator/src/test/java/org/keycloak/operator/utils/K8sUtils.java b/operator/src/test/java/org/keycloak/operator/utils/K8sUtils.java index 4d41b6e7a2..30fc9d1090 100644 --- a/operator/src/test/java/org/keycloak/operator/utils/K8sUtils.java +++ b/operator/src/test/java/org/keycloak/operator/utils/K8sUtils.java @@ -18,24 +18,24 @@ package org.keycloak.operator.utils; import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Serialization; -import io.quarkus.kubernetes.client.runtime.KubernetesClientUtils; import io.quarkus.logging.Log; import org.awaitility.Awaitility; import org.keycloak.operator.v2alpha1.crds.Keycloak; import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.UUID; import static java.util.concurrent.TimeUnit.MINUTES; -import static org.assertj.core.api.Assertions.assertThat; /** * @author Vaclav Muzikar @@ -54,8 +54,14 @@ public final class K8sUtils { return getResourceFromMultiResourceFile("example-keycloak.yml", 0); } + public static Secret getDefaultTlsSecret() { + return getResourceFromMultiResourceFile("example-keycloak.yml", 2); + } + + public static void deployKeycloak(KubernetesClient client, Keycloak kc, boolean waitUntilReady) { - client.resources(Keycloak.class).createOrReplace(kc); + client.resources(Keycloak.class).inNamespace(kc.getMetadata().getNamespace()).createOrReplace(kc); + client.secrets().inNamespace(kc.getMetadata().getNamespace()).createOrReplace(getDefaultTlsSecret()); if (waitUntilReady) { waitForKeycloakToBeReady(client, kc); @@ -69,16 +75,23 @@ public final class K8sUtils { public static void waitForKeycloakToBeReady(KubernetesClient client, Keycloak kc) { Log.infof("Waiting for Keycloak \"%s\"", kc.getMetadata().getName()); Awaitility.await() + .pollInterval(Duration.ofSeconds(1)) + .timeout(Duration.ofMinutes(5)) .ignoreExceptions() .untilAsserted(() -> { - var currentKc = client.resources(Keycloak.class).withName(kc.getMetadata().getName()).get(); + var currentKc = client + .resources(Keycloak.class) + .inNamespace(kc.getMetadata().getNamespace()) + .withName(kc.getMetadata().getName()) + .get(); + CRAssert.assertKeycloakStatusCondition(currentKc, KeycloakStatusCondition.READY, true); CRAssert.assertKeycloakStatusCondition(currentKc, KeycloakStatusCondition.HAS_ERRORS, false); }); } public static String inClusterCurl(KubernetesClient k8sclient, String namespace, String url) { - return inClusterCurl(k8sclient, namespace, "-s", "-o", "/dev/null", "-w", "%{http_code}", url); + return inClusterCurl(k8sclient, namespace, "--insecure", "-s", "-o", "/dev/null", "-w", "%{http_code}", url); } public static String inClusterCurl(KubernetesClient k8sclient, String namespace, String... args) { @@ -93,7 +106,7 @@ public final class K8sUtils { .build()) .done(); Log.info("Waiting for curl Pod to finish running"); - Awaitility.await().atMost(2, MINUTES) + Awaitility.await().atMost(3, MINUTES) .until(() -> { String phase = k8sclient.pods().inNamespace(namespace).withName(podName).get() @@ -111,7 +124,7 @@ public final class K8sUtils { } finally { Log.info("Deleting curl Pod"); k8sclient.pods().inNamespace(namespace).withName(podName).delete(); - Awaitility.await().atMost(1, MINUTES) + Awaitility.await().atMost(2, MINUTES) .until(() -> k8sclient.pods().inNamespace(namespace).withName(podName) .get() == null); } diff --git a/operator/src/test/resources/correct-podtemplate-keycloak.yml b/operator/src/test/resources/correct-podtemplate-keycloak.yml index 9457014d1c..91dfcb9344 100644 --- a/operator/src/test/resources/correct-podtemplate-keycloak.yml +++ b/operator/src/test/resources/correct-podtemplate-keycloak.yml @@ -9,6 +9,8 @@ spec: KC_DB_URL_HOST: postgres-db KC_DB_USERNAME: postgres KC_DB_PASSWORD: testpassword + hostname: example.com + tlsSecret: INSECURE-DISABLE unsupported: podTemplate: metadata: diff --git a/operator/src/test/resources/empty-podtemplate-keycloak.yml b/operator/src/test/resources/empty-podtemplate-keycloak.yml index 4a99528823..69d6429ff0 100644 --- a/operator/src/test/resources/empty-podtemplate-keycloak.yml +++ b/operator/src/test/resources/empty-podtemplate-keycloak.yml @@ -9,5 +9,7 @@ spec: KC_DB_URL_HOST: postgres-db KC_DB_USERNAME: postgres KC_DB_PASSWORD: testpassword + hostname: example.com + tlsSecret: INSECURE-DISABLE unsupported: podTemplate: