TLS config in the operator

This commit is contained in:
andreaTP 2022-03-03 12:32:23 +00:00 committed by Bruno Oliveira da Silva
parent 1710b38cf8
commit fd2cd688b8
15 changed files with 309 additions and 46 deletions

View file

@ -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_FOLDER = "/opt/extensions";
public static final String INIT_CONTAINER_EXTENSIONS_ENV_VAR = "KEYCLOAK_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_PROTOCOL = "TCP";
public static final String KEYCLOAK_SERVICE_SUFFIX = "-service"; public static final String KEYCLOAK_SERVICE_SUFFIX = "-service";
public static final Integer KEYCLOAK_DISCOVERY_SERVICE_PORT = 7800; public static final Integer KEYCLOAK_DISCOVERY_SERVICE_PORT = 7800;
public static final String KEYCLOAK_DISCOVERY_SERVICE_SUFFIX = "-discovery"; 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";
} }

View file

@ -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() { private Deployment createBaseDeployment() {
var is = this.getClass().getResourceAsStream("/base-keycloak-deployment.yaml"); var is = this.getClass().getResourceAsStream("/base-keycloak-deployment.yaml");
Deployment baseDeployment = Serialization.unmarshal(is, Deployment.class); Deployment baseDeployment = Serialization.unmarshal(is, Deployment.class);
@ -371,6 +446,8 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
container.setEnv(getEnvVars()); container.setEnv(getEnvVars());
configureHostname(baseDeployment);
configureTLS(baseDeployment);
addInitContainer(baseDeployment, keycloakCR.getSpec().getExtensions()); addInitContainer(baseDeployment, keycloakCR.getSpec().getExtensions());
mergePodTemplate(baseDeployment.getSpec().getTemplate()); mergePodTemplate(baseDeployment.getSpec().getTemplate());

View file

@ -83,7 +83,7 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
.get(); .get();
} }
private Job buildJob(Container keycloakContainer, Volume secretVolume) { private Job buildJob(Container keycloakContainer, List<Volume> volumes) {
return new JobBuilder() return new JobBuilder()
.withNewMetadata() .withNewMetadata()
.withName(getName()) .withName(getName())
@ -93,7 +93,7 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
.withNewTemplate() .withNewTemplate()
.withNewSpec() .withNewSpec()
.withContainers(keycloakContainer) .withContainers(keycloakContainer)
.addToVolumes(secretVolume) .withVolumes(volumes)
.withRestartPolicy("Never") .withRestartPolicy("Never")
.endSpec() .endSpec()
.endTemplate() .endTemplate()
@ -112,8 +112,10 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
private Job createImportJob() { private Job createImportJob() {
var keycloakContainer = buildKeycloakJobContainer(); 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; return importJob;
} }
@ -142,14 +144,13 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
.setCommand(command); .setCommand(command);
keycloakContainer keycloakContainer
.setArgs(commandArgs); .setArgs(commandArgs);
var volumeMounts = List.of( var volumeMount = new VolumeMountBuilder()
new VolumeMountBuilder() .withName(volumeName)
.withName(volumeName) .withReadOnly(true)
.withReadOnly(true) .withMountPath(importMntPath)
.withMountPath(importMntPath) .build();
.build());
keycloakContainer.setVolumeMounts(volumeMounts); keycloakContainer.getVolumeMounts().add(volumeMount);
// Disable probes since we are not really starting the server // Disable probes since we are not really starting the server
keycloakContainer.setReadinessProbe(null); keycloakContainer.setReadinessProbe(null);

View file

@ -17,7 +17,6 @@
package org.keycloak.operator.v2alpha1; package org.keycloak.operator.v2alpha1;
import io.fabric8.kubernetes.api.model.HasMetadata; 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.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.ServiceSpec; import io.fabric8.kubernetes.api.model.ServiceSpec;
@ -34,16 +33,19 @@ import java.util.Optional;
public class KeycloakService extends OperatorManagedResource implements StatusUpdater<KeycloakStatusBuilder> { public class KeycloakService extends OperatorManagedResource implements StatusUpdater<KeycloakStatusBuilder> {
private Service existingService; private Service existingService;
private final Keycloak keycloak;
public KeycloakService(KubernetesClient client, Keycloak keycloakCR) { public KeycloakService(KubernetesClient client, Keycloak keycloakCR) {
super(client, keycloakCR); super(client, keycloakCR);
this.keycloak = keycloakCR;
this.existingService = fetchExistingService(); this.existingService = fetchExistingService();
} }
private ServiceSpec getServiceSpec() { private ServiceSpec getServiceSpec() {
var port = (this.keycloak.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT;
return new ServiceSpecBuilder() return new ServiceSpecBuilder()
.addNewPort() .addNewPort()
.withPort(Constants.KEYCLOAK_SERVICE_PORT) .withPort(port)
.withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL)
.endPort() .endPort()
.withSelector(Constants.DEFAULT_LABELS) .withSelector(Constants.DEFAULT_LABELS)

View file

@ -18,8 +18,10 @@ package org.keycloak.operator.v2alpha1.crds;
import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.keycloak.operator.Constants;
import org.keycloak.operator.v2alpha1.crds.keycloakspec.Unsupported; import org.keycloak.operator.v2alpha1.crds.keycloakspec.Unsupported;
import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -28,6 +30,16 @@ public class KeycloakSpec {
private int instances = 1; private int instances = 1;
private String image; private String image;
private Map<String, String> serverConfiguration; private Map<String, String> 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.") @JsonPropertyDescription("List of URLs to download Keycloak extensions.")
private List<String> extensions; private List<String> extensions;
@JsonPropertyDescription( @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.") "Use at your own risk and open an issue with your use-case if you don't find an alternative way.")
private Unsupported unsupported; 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<String> getExtensions() { public List<String> getExtensions() {
return extensions; return extensions;
} }

View file

@ -18,7 +18,8 @@ spec:
spec: spec:
containers: containers:
- args: - args:
- start-dev - start
- --auto-build
imagePullPolicy: Always imagePullPolicy: Always
name: keycloak name: keycloak
ports: ports:
@ -27,16 +28,26 @@ spec:
- containerPort: 8080 - containerPort: 8080
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: exec:
path: /health/live command:
port: 8080 - curl
- --insecure
- --head
- --fail
- --silent
- https://127.0.0.1:8443/health/live
initialDelaySeconds: 20 initialDelaySeconds: 20
periodSeconds: 2 periodSeconds: 2
failureThreshold: 100 failureThreshold: 100
readinessProbe: readinessProbe:
httpGet: exec:
path: /health/ready command:
port: 8080 - curl
- --insecure
- --head
- --fail
- --silent
- https://127.0.0.1:8443/health/ready
initialDelaySeconds: 20 initialDelaySeconds: 20
periodSeconds: 2 periodSeconds: 2
failureThreshold: 200 failureThreshold: 200

View file

@ -11,6 +11,8 @@ spec:
# KC_DB_PASSWORD: ${secret:keycloak-db-secret:password} # KC_DB_PASSWORD: ${secret:keycloak-db-secret:password}
KC_DB_USERNAME: postgres KC_DB_USERNAME: postgres
KC_DB_PASSWORD: testpassword KC_DB_PASSWORD: testpassword
hostname: example.com
tlsSecret: example-tls-secret
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@ -20,3 +22,12 @@ data:
username: cG9zdGdyZXM= # postgres username: cG9zdGdyZXM= # postgres
password: dGVzdHBhc3N3b3Jk # testpassword password: dGVzdHBhc3N3b3Jk # testpassword
type: Opaque type: Opaque
---
apiVersion: v1
kind: Secret
metadata:
name: example-tls-secret
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVmekNDQXVlZ0F3SUJBZ0lSQUlVenBxa1FoaTNKclZBcmxVNVRhVTB3RFFZSktvWklodmNOQVFFTEJRQXcKZ1lreEhqQWNCZ05WQkFvVEZXMXJZMlZ5ZENCa1pYWmxiRzl3YldWdWRDQkRRVEV2TUMwR0ExVUVDd3dtWVhCbApjblZtWm05QVlYQmxjblZtWm04dGJXRmpJQ2hCYm1SeVpXRWdVR1Z5ZFdabWJ5a3hOakEwQmdOVkJBTU1MVzFyClkyVnlkQ0JoY0dWeWRXWm1iMEJoY0dWeWRXWm1ieTF0WVdNZ0tFRnVaSEpsWVNCUVpYSjFabVp2S1RBZUZ3MHkKTWpBek1ETXhNVEExTlRWYUZ3MHlOREEyTURNeE1EQTFOVFZhTUZveEp6QWxCZ05WQkFvVEhtMXJZMlZ5ZENCawpaWFpsYkc5d2JXVnVkQ0JqWlhKMGFXWnBZMkYwWlRFdk1DMEdBMVVFQ3d3bVlYQmxjblZtWm05QVlYQmxjblZtClptOHRiV0ZqSUNoQmJtUnlaV0VnVUdWeWRXWm1ieWt3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXcKZ2dFS0FvSUJBUUN5MjljQ0JrSzZNWERNbWZONy9TVmdiNXR2WXFWc01LVjhjaEwvTE5UcXVkdVA0QVBZeEtzMApQWnZBd0RRa3lGUXRxQlVvTXBhelBCaUpyREZ2eHc2VDZaeGVUOXlobCtvNWxhVmdseUdUMC9TcTBjTkg3UkZaCk5KeXpEZDdhREVjc2E0cmZmVEJPbk9UZjZ3QzhuSkNobTl4Mm9FWlU0UHRIb2tKZzcrVlFXYUdVRHg3Wm5YSlgKUXQ5SXFSb1dQWW1BWnNQc1FUNzdPeWkzUGZSa2NqZ1FTWEJsWVhNWXFZOWxMZTZpR2NldnNkdGhyOEdOZFF4dQpJV3RBOTYwdkgzSFpwRmgyRXRJbnVEOTdlWjU4STB4WXZuU2xSZGlXV1BPSTNwWDFvR0xyWDZjWGl1RlRDNUg3ClB3NnVSZUdVZ2tvR2tXS1pSU3RZdGp1dENuZHEvZ2JuQWdNQkFBR2pnWTh3Z1l3d0RnWURWUjBQQVFIL0JBUUQKQWdXZ01CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUJNQjhHQTFVZEl3UVlNQmFBRkg2Qmh5V21zVEpwMTdqSApVLzlKaDI1MUdhMTFNRVFHQTFVZEVRUTlNRHVDQzJWNFlXMXdiR1V1WTI5dGdnbHRlV0Z3Y0M1a1pYYUNDV3h2ClkyRnNhRzl6ZEljRWZ3QUFBWWNRQUFBQUFBQUFBQUFBQUFBQUFBQUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0MKQVlFQWYrazRMQW11YjlLKzM3RWo5M3RwYXhZdER2cUl4d1VpVkRHUyt6TElrd296akkyaHVTYko2N0lsdVJZaQp0SjVUU3hlM1hMTTNJM1NQU2tKNUxpY0JLRjJDRW1tdDBKRnk2WERxeU80L3NncFVDWVh6V3J1ZWU5VWM4VkhNCnljL3ZLclN3bTVDek82alIyZk0xajdCUWVJdHh6Qk1rTlJYZUUxSUVJWGtYMUFFUGRYaFBHZXFya1NqYzdGbjkKSkIzeGIvN0xvdTNxSFlBV2xyeThicWd2Z0pjZFlVWE9RWlVZSXE0ekd4bkNZRFRTblRuTG8vbW5YQ0h6MHZXRApldlpRQzhsL2t2TWRNb1RNSUxWamxObFgyeTNyekw2ak1QZTIxcGpSdFd3K0R6S1E1dkdZemMxL1hFbXJRaVJVCmxlRWE4cVp4QVkySXptMW9hTWdNa0cwZklKRkEyZk9DSGVWTnJOek93S1ZjaXFGVHpUanpZMW9HZDd5bncrQ28KaUF1Tm03TERxdzczakJYMVBBK1ZYM0pnRTVlODVnQ0FVU0UzK0Y3Z1RGb1hBS1M3T255Mk9mS0xSREw3U0NPWgp1THlub1NVeTUrcnJlUjBJNzRwTXVhRm9hUHo5U2lCNzVCNnZ4eGZWV0xLN0g3T1ZxV1YyR0Qra3dxSW1hOUVJClVmV2IKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ3kyOWNDQmtLNk1YRE0KbWZONy9TVmdiNXR2WXFWc01LVjhjaEwvTE5UcXVkdVA0QVBZeEtzMFBadkF3RFFreUZRdHFCVW9NcGF6UEJpSgpyREZ2eHc2VDZaeGVUOXlobCtvNWxhVmdseUdUMC9TcTBjTkg3UkZaTkp5ekRkN2FERWNzYTRyZmZUQk9uT1RmCjZ3QzhuSkNobTl4Mm9FWlU0UHRIb2tKZzcrVlFXYUdVRHg3Wm5YSlhRdDlJcVJvV1BZbUFac1BzUVQ3N095aTMKUGZSa2NqZ1FTWEJsWVhNWXFZOWxMZTZpR2NldnNkdGhyOEdOZFF4dUlXdEE5NjB2SDNIWnBGaDJFdEludUQ5NwplWjU4STB4WXZuU2xSZGlXV1BPSTNwWDFvR0xyWDZjWGl1RlRDNUg3UHc2dVJlR1Vna29Ha1dLWlJTdFl0anV0CkNuZHEvZ2JuQWdNQkFBRUNnZ0VBWEtWSlV2QWhRa2IzMGROdzd1bXFvYkJPQ0QxRnlLdk9ISThPVGdWUDZLSUwKSEJTQ2laY2R3M3FpSWc2dE05eGMxaVY1aUEva1JjVThSSnZnSTdFdFdPcXFKNlFnZWNleCtOQU9FT0ZYOERYYgpSMXhPVmdSemR3eXNtb2IxeDJhU3UyeWRTN1NTQURaK3k0bjBJTDdNb0JtVzhnK0ZQdFFtOU8wVWl4ZllaV3lhCmVleHFOS0xLVS9neG5iZXIvQy9kVWpPS3dndmpDRHkvZjhGQ1BNcDBEZzFLdU1Uc2J5ZjRyczQvM1JkUDBtK08KdXZhTTJQaEJsNEJJQVg2NXRIc1p6TGRtZWhOdzd1RGR1eGhBenVwVkR6YlhKcGQ5cEZaWE83QzlGWXhVNFpuSgpHbnliWktQcDlrL28yVkw2OWR1d0NSdkYySlVWdEZQZ2pibG80R2o4Y1FLQmdRRFc3NXVhdGtHUmNHR1Y3QWE3CktWcXVwWXFoazlxOERvaG9CZ2xvZzZMb3REMzFxbks2b1hwM2UweS9mZElMaEFERGdCaStEYW05aGFicEV4Q3YKK29TcnVNbFJLM1EyN1ZzbFd6WVQ2K0JyZDRNS3RrNjN6TG1YS25iTWhsOE9TQ1FERmdrUXQrWExGak1FUmNmawpvb2JWem1qajdrWnh5c1hoV2xsdlFTaXkyUUtCZ1FEVkI3ZG9oNDcwZ3I2VjBvbko4VzlDazF6MTBWMEtCQ0ZjCmFkd3Z4UjBKdUhsc2ZmVVJsaU1zQ3VzQlYzWDJpb3liblNxeG14SGQ0Qm5zeWx4bFlLdEpkM2pQbE05bnVoajAKbWZwMzFIcEN6aWRZRUs5Q1RVVFBTZE5tcUlFdHJqTkppano0OHcxNWlTVFA2c2c4ZXhWVUdtTVkrUDVyeDM4SQpXSkxzU3VqdnZ3S0JnUUNrTW5QN0l4VEFHTXhVRGZXdWNZODNNSHZScC9SSUNnb20vY1dlTkVIMTZBd1ZhdHN1CnZFR2ttV3N1TnQ2SnNaUXJ4ZVlnK3FzYmY4amM4WldqK292ejY3elA1NVJtaWJsQnRvWi9mWWo2VUZpcGpGQmkKbFdHS25BUVpodVdETVpWaFRpb3F2WEl0VFk0M3kxOUR5TzJjMUl6STQ3U3BKYkU1MFIzVm9qK0hNUUtCZ0VkZwpESDJEWGN4aXVnUnN4Q25iTU5IM21kL3F3K2VGTnNCRjM3WkpyczhBOWYzNXZkQ2tveWd3aUVpc3l5Tk5qSXJlCi85ejkvZUIvSTNDSTVLZzYyV2tHRkg1SWQ2MWpWdFV0ZWhRSUp1YVhOK3R6dTZUVlNzYkJENG1IejdCRWUzNmEKU0krSXIrMFduRFRsankxa2QrTHo3RndEb1FydmpvcDNVdExFem9MMUFvR0JBTTcvWVRNWSszV1NDeENPL3NIWAo3OGZDeHhBRHFMVWMxVURYdGMzcFhKQnorL3hJeUx1Q3JQYnlsUC82L21yRjN4SENTbGg3bi9mcFovV1dRMzIxCjNyZnR5Y2czWWVzalZxdjBaZmJVb01OdFE5cGYrcFpQMGpWVEZXMlF3YTZWYURrcGdTQnB4QzlvWXlMWTRldGMKajBkWm9NeTVMYXNKcm5jUjhlTVc4NHlnCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
type: kubernetes.io/tls

View file

@ -181,7 +181,7 @@ public abstract class ClusterOperatorTest {
private static void setDefaultAwaitilityTimings() { private static void setDefaultAwaitilityTimings() {
Awaitility.setDefaultPollInterval(Duration.ofSeconds(1)); Awaitility.setDefaultPollInterval(Duration.ofSeconds(1));
Awaitility.setDefaultTimeout(Duration.ofSeconds(240)); Awaitility.setDefaultTimeout(Duration.ofSeconds(360));
} }
@AfterEach @AfterEach

View file

@ -15,7 +15,6 @@ import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport;
import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition; import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition;
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition; import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition;
import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
@ -69,7 +68,7 @@ public class ClusteringE2EIT extends ClusterOperatorTest {
// get the service // get the service
var service = new KeycloakService(k8sclient, kc); 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(() -> { Awaitility.await().atMost(5, MINUTES).untilAsserted(() -> {
Log.info("Starting curl Pod to test if the realm is available"); Log.info("Starting curl Pod to test if the realm is available");
@ -98,6 +97,7 @@ public class ClusteringE2EIT extends ClusterOperatorTest {
.resources(Keycloak.class) .resources(Keycloak.class)
.inNamespace(kc.getMetadata().getNamespace()) .inNamespace(kc.getMetadata().getNamespace())
.withName(kc.getMetadata().getName()); .withName(kc.getMetadata().getName());
K8sUtils.deployKeycloak(k8sclient, kc, false);
var targetInstances = 3; var targetInstances = 3;
kc.getSpec().setInstances(targetInstances); kc.getSpec().setInstances(targetInstances);
k8sclient.resources(Keycloak.class).inNamespace(namespace).createOrReplace(kc); k8sclient.resources(Keycloak.class).inNamespace(namespace).createOrReplace(kc);
@ -136,14 +136,15 @@ public class ClusteringE2EIT extends ClusterOperatorTest {
.pods() .pods()
.inNamespace(namespace) .inNamespace(namespace)
.withName(pod.getMetadata().getName()) .withName(pod.getMetadata().getName())
.portForward(8080, 8080)) { .portForward(8443, 8443)) {
token = (token != null) ? token : RestAssured.given() token = (token != null) ? token : RestAssured.given()
.relaxedHTTPSValidation()
.param("grant_type" , "password") .param("grant_type" , "password")
.param("client_id", "token-test-client") .param("client_id", "token-test-client")
.param("username", "test") .param("username", "test")
.param("password", "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() .body()
.jsonPath() .jsonPath()
.getString("access_token"); .getString("access_token");
@ -151,8 +152,9 @@ public class ClusteringE2EIT extends ClusterOperatorTest {
Log.info("Using token:" + token); Log.info("Using token:" + token);
var username = RestAssured.given() var username = RestAssured.given()
.relaxedHTTPSValidation()
.header("Authorization", "Bearer " + token) .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() .body()
.jsonPath() .jsonPath()
.getString("preferred_username"); .getString("preferred_username");
@ -174,23 +176,22 @@ public class ClusteringE2EIT extends ClusterOperatorTest {
for (int i = 0; i < (targetInstances * 2); i++) { for (int i = 0; i < (targetInstances * 2); i++) {
if (token2 == null) { 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); 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); Log.info("Curl Output with token: " + tokenOutput);
JsonNode tokenAnswer = Serialization.jsonMapper().readTree(tokenOutput); JsonNode tokenAnswer = Serialization.jsonMapper().readTree(tokenOutput);
assertThat(tokenAnswer.hasNonNull("access_token")).isTrue(); assertThat(tokenAnswer.hasNonNull("access_token")).isTrue();
token2 = tokenAnswer.get("access_token").asText(); 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); 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); Log.info("Curl Output on access attempt: " + curlOutput);
JsonNode answer = Serialization.jsonMapper().readTree(curlOutput); JsonNode answer = Serialization.jsonMapper().readTree(curlOutput);
assertThat(answer.hasNonNull("preferred_username")).isTrue(); assertThat(answer.hasNonNull("preferred_username")).isTrue();
assertThat(answer.get("preferred_username").asText()).isEqualTo("test"); assertThat(answer.get("preferred_username").asText()).isEqualTo("test");

View file

@ -6,6 +6,8 @@ import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; 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 org.keycloak.operator.v2alpha1.crds.Keycloak;
import java.time.Duration; import java.time.Duration;
@ -14,6 +16,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; 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.junit.jupiter.api.Assertions.assertTrue;
import static org.keycloak.operator.Constants.DEFAULT_LABELS; import static org.keycloak.operator.Constants.DEFAULT_LABELS;
import static org.keycloak.operator.utils.K8sUtils.deployKeycloak; 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("<a href=\"https://example.com/admin/\">"));
});
} 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("<a href=\"https://foo.bar/admin/\">"));
});
} catch (Exception e) {
savePodLogs();
throw e;
}
}
} }

View file

@ -43,6 +43,8 @@ public class PodTemplateTest {
var kc = new Keycloak(); var kc = new Keycloak();
var spec = new KeycloakSpec(); var spec = new KeycloakSpec();
spec.setUnsupported(new Unsupported(podTemplate)); spec.setUnsupported(new Unsupported(podTemplate));
spec.setHostname("example.com");
spec.setTlsSecret("example-tls-secret");
kc.setSpec(spec); kc.setSpec(spec);
var deployment = new KeycloakDeployment(null, config, kc, new Deployment()); var deployment = new KeycloakDeployment(null, config, kc, new Deployment());
return (Deployment) deployment.getReconciledResource().get(); return (Deployment) deployment.getReconciledResource().get();
@ -98,7 +100,7 @@ public class PodTemplateTest {
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate(); var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert // Assert
assertEquals(volumeName, podTemplate.getSpec().getVolumes().get(0).getName()); assertEquals(volumeName, podTemplate.getSpec().getVolumes().get(1).getName());
} }
@Test @Test
@ -121,8 +123,8 @@ public class PodTemplateTest {
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate(); var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert // Assert
assertEquals(volumeMountName, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(0).getName()); assertEquals(volumeMountName, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(1).getName());
assertEquals(volumeMountPath, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(0).getMountPath()); assertEquals(volumeMountPath, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(1).getMountPath());
} }
@Test @Test

View file

@ -11,7 +11,7 @@ import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat; 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.getDefaultKeycloakDeployment;
import static org.keycloak.operator.utils.K8sUtils.inClusterCurl; import static org.keycloak.operator.utils.K8sUtils.inClusterCurl;
import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.DONE; import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.DONE;
@ -55,9 +55,9 @@ public class RealmImportE2EIT extends ClusterOperatorTest {
}); });
var service = new KeycloakService(k8sclient, getDefaultKeycloakDeployment()); var service = new KeycloakService(k8sclient, getDefaultKeycloakDeployment());
String url = 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("Starting curl Pod to test if the realm is available");
Log.info("Url: '" + url + "'"); Log.info("Url: '" + url + "'");
String curlOutput = inClusterCurl(k8sclient, namespace, url); String curlOutput = inClusterCurl(k8sclient, namespace, url);

View file

@ -18,24 +18,24 @@
package org.keycloak.operator.utils; package org.keycloak.operator.utils;
import io.fabric8.kubernetes.api.model.Pod; 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.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.kubernetes.client.runtime.KubernetesClientUtils;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.keycloak.operator.v2alpha1.crds.Keycloak; import org.keycloak.operator.v2alpha1.crds.Keycloak;
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition; import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition;
import java.time.Duration;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
@ -54,8 +54,14 @@ public final class K8sUtils {
return getResourceFromMultiResourceFile("example-keycloak.yml", 0); 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) { 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) { if (waitUntilReady) {
waitForKeycloakToBeReady(client, kc); waitForKeycloakToBeReady(client, kc);
@ -69,16 +75,23 @@ public final class K8sUtils {
public static void waitForKeycloakToBeReady(KubernetesClient client, Keycloak kc) { public static void waitForKeycloakToBeReady(KubernetesClient client, Keycloak kc) {
Log.infof("Waiting for Keycloak \"%s\"", kc.getMetadata().getName()); Log.infof("Waiting for Keycloak \"%s\"", kc.getMetadata().getName());
Awaitility.await() Awaitility.await()
.pollInterval(Duration.ofSeconds(1))
.timeout(Duration.ofMinutes(5))
.ignoreExceptions() .ignoreExceptions()
.untilAsserted(() -> { .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.READY, true);
CRAssert.assertKeycloakStatusCondition(currentKc, KeycloakStatusCondition.HAS_ERRORS, false); CRAssert.assertKeycloakStatusCondition(currentKc, KeycloakStatusCondition.HAS_ERRORS, false);
}); });
} }
public static String inClusterCurl(KubernetesClient k8sclient, String namespace, String url) { 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) { public static String inClusterCurl(KubernetesClient k8sclient, String namespace, String... args) {
@ -93,7 +106,7 @@ public final class K8sUtils {
.build()) .build())
.done(); .done();
Log.info("Waiting for curl Pod to finish running"); Log.info("Waiting for curl Pod to finish running");
Awaitility.await().atMost(2, MINUTES) Awaitility.await().atMost(3, MINUTES)
.until(() -> { .until(() -> {
String phase = String phase =
k8sclient.pods().inNamespace(namespace).withName(podName).get() k8sclient.pods().inNamespace(namespace).withName(podName).get()
@ -111,7 +124,7 @@ public final class K8sUtils {
} finally { } finally {
Log.info("Deleting curl Pod"); Log.info("Deleting curl Pod");
k8sclient.pods().inNamespace(namespace).withName(podName).delete(); k8sclient.pods().inNamespace(namespace).withName(podName).delete();
Awaitility.await().atMost(1, MINUTES) Awaitility.await().atMost(2, MINUTES)
.until(() -> k8sclient.pods().inNamespace(namespace).withName(podName) .until(() -> k8sclient.pods().inNamespace(namespace).withName(podName)
.get() == null); .get() == null);
} }

View file

@ -9,6 +9,8 @@ spec:
KC_DB_URL_HOST: postgres-db KC_DB_URL_HOST: postgres-db
KC_DB_USERNAME: postgres KC_DB_USERNAME: postgres
KC_DB_PASSWORD: testpassword KC_DB_PASSWORD: testpassword
hostname: example.com
tlsSecret: INSECURE-DISABLE
unsupported: unsupported:
podTemplate: podTemplate:
metadata: metadata:

View file

@ -9,5 +9,7 @@ spec:
KC_DB_URL_HOST: postgres-db KC_DB_URL_HOST: postgres-db
KC_DB_USERNAME: postgres KC_DB_USERNAME: postgres
KC_DB_PASSWORD: testpassword KC_DB_PASSWORD: testpassword
hostname: example.com
tlsSecret: INSECURE-DISABLE
unsupported: unsupported:
podTemplate: podTemplate: