TLS config in the operator
This commit is contained in:
parent
1710b38cf8
commit
fd2cd688b8
15 changed files with 309 additions and 46 deletions
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
|
|||
.get();
|
||||
}
|
||||
|
||||
private Job buildJob(Container keycloakContainer, Volume secretVolume) {
|
||||
private Job buildJob(Container keycloakContainer, List<Volume> 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()
|
||||
var volumeMount = new VolumeMountBuilder()
|
||||
.withName(volumeName)
|
||||
.withReadOnly(true)
|
||||
.withMountPath(importMntPath)
|
||||
.build());
|
||||
.build();
|
||||
|
||||
keycloakContainer.setVolumeMounts(volumeMounts);
|
||||
keycloakContainer.getVolumeMounts().add(volumeMount);
|
||||
|
||||
// Disable probes since we are not really starting the server
|
||||
keycloakContainer.setReadinessProbe(null);
|
||||
|
|
|
@ -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<KeycloakStatusBuilder> {
|
||||
|
||||
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)
|
||||
|
|
|
@ -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<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.")
|
||||
private List<String> 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<String> getExtensions() {
|
||||
return extensions;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -20,3 +22,12 @@ data:
|
|||
username: cG9zdGdyZXM= # postgres
|
||||
password: dGVzdHBhc3N3b3Jk # testpassword
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: example-tls-secret
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVmekNDQXVlZ0F3SUJBZ0lSQUlVenBxa1FoaTNKclZBcmxVNVRhVTB3RFFZSktvWklodmNOQVFFTEJRQXcKZ1lreEhqQWNCZ05WQkFvVEZXMXJZMlZ5ZENCa1pYWmxiRzl3YldWdWRDQkRRVEV2TUMwR0ExVUVDd3dtWVhCbApjblZtWm05QVlYQmxjblZtWm04dGJXRmpJQ2hCYm1SeVpXRWdVR1Z5ZFdabWJ5a3hOakEwQmdOVkJBTU1MVzFyClkyVnlkQ0JoY0dWeWRXWm1iMEJoY0dWeWRXWm1ieTF0WVdNZ0tFRnVaSEpsWVNCUVpYSjFabVp2S1RBZUZ3MHkKTWpBek1ETXhNVEExTlRWYUZ3MHlOREEyTURNeE1EQTFOVFZhTUZveEp6QWxCZ05WQkFvVEhtMXJZMlZ5ZENCawpaWFpsYkc5d2JXVnVkQ0JqWlhKMGFXWnBZMkYwWlRFdk1DMEdBMVVFQ3d3bVlYQmxjblZtWm05QVlYQmxjblZtClptOHRiV0ZqSUNoQmJtUnlaV0VnVUdWeWRXWm1ieWt3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXcKZ2dFS0FvSUJBUUN5MjljQ0JrSzZNWERNbWZONy9TVmdiNXR2WXFWc01LVjhjaEwvTE5UcXVkdVA0QVBZeEtzMApQWnZBd0RRa3lGUXRxQlVvTXBhelBCaUpyREZ2eHc2VDZaeGVUOXlobCtvNWxhVmdseUdUMC9TcTBjTkg3UkZaCk5KeXpEZDdhREVjc2E0cmZmVEJPbk9UZjZ3QzhuSkNobTl4Mm9FWlU0UHRIb2tKZzcrVlFXYUdVRHg3Wm5YSlgKUXQ5SXFSb1dQWW1BWnNQc1FUNzdPeWkzUGZSa2NqZ1FTWEJsWVhNWXFZOWxMZTZpR2NldnNkdGhyOEdOZFF4dQpJV3RBOTYwdkgzSFpwRmgyRXRJbnVEOTdlWjU4STB4WXZuU2xSZGlXV1BPSTNwWDFvR0xyWDZjWGl1RlRDNUg3ClB3NnVSZUdVZ2tvR2tXS1pSU3RZdGp1dENuZHEvZ2JuQWdNQkFBR2pnWTh3Z1l3d0RnWURWUjBQQVFIL0JBUUQKQWdXZ01CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUJNQjhHQTFVZEl3UVlNQmFBRkg2Qmh5V21zVEpwMTdqSApVLzlKaDI1MUdhMTFNRVFHQTFVZEVRUTlNRHVDQzJWNFlXMXdiR1V1WTI5dGdnbHRlV0Z3Y0M1a1pYYUNDV3h2ClkyRnNhRzl6ZEljRWZ3QUFBWWNRQUFBQUFBQUFBQUFBQUFBQUFBQUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0MKQVlFQWYrazRMQW11YjlLKzM3RWo5M3RwYXhZdER2cUl4d1VpVkRHUyt6TElrd296akkyaHVTYko2N0lsdVJZaQp0SjVUU3hlM1hMTTNJM1NQU2tKNUxpY0JLRjJDRW1tdDBKRnk2WERxeU80L3NncFVDWVh6V3J1ZWU5VWM4VkhNCnljL3ZLclN3bTVDek82alIyZk0xajdCUWVJdHh6Qk1rTlJYZUUxSUVJWGtYMUFFUGRYaFBHZXFya1NqYzdGbjkKSkIzeGIvN0xvdTNxSFlBV2xyeThicWd2Z0pjZFlVWE9RWlVZSXE0ekd4bkNZRFRTblRuTG8vbW5YQ0h6MHZXRApldlpRQzhsL2t2TWRNb1RNSUxWamxObFgyeTNyekw2ak1QZTIxcGpSdFd3K0R6S1E1dkdZemMxL1hFbXJRaVJVCmxlRWE4cVp4QVkySXptMW9hTWdNa0cwZklKRkEyZk9DSGVWTnJOek93S1ZjaXFGVHpUanpZMW9HZDd5bncrQ28KaUF1Tm03TERxdzczakJYMVBBK1ZYM0pnRTVlODVnQ0FVU0UzK0Y3Z1RGb1hBS1M3T255Mk9mS0xSREw3U0NPWgp1THlub1NVeTUrcnJlUjBJNzRwTXVhRm9hUHo5U2lCNzVCNnZ4eGZWV0xLN0g3T1ZxV1YyR0Qra3dxSW1hOUVJClVmV2IKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ3kyOWNDQmtLNk1YRE0KbWZONy9TVmdiNXR2WXFWc01LVjhjaEwvTE5UcXVkdVA0QVBZeEtzMFBadkF3RFFreUZRdHFCVW9NcGF6UEJpSgpyREZ2eHc2VDZaeGVUOXlobCtvNWxhVmdseUdUMC9TcTBjTkg3UkZaTkp5ekRkN2FERWNzYTRyZmZUQk9uT1RmCjZ3QzhuSkNobTl4Mm9FWlU0UHRIb2tKZzcrVlFXYUdVRHg3Wm5YSlhRdDlJcVJvV1BZbUFac1BzUVQ3N095aTMKUGZSa2NqZ1FTWEJsWVhNWXFZOWxMZTZpR2NldnNkdGhyOEdOZFF4dUlXdEE5NjB2SDNIWnBGaDJFdEludUQ5NwplWjU4STB4WXZuU2xSZGlXV1BPSTNwWDFvR0xyWDZjWGl1RlRDNUg3UHc2dVJlR1Vna29Ha1dLWlJTdFl0anV0CkNuZHEvZ2JuQWdNQkFBRUNnZ0VBWEtWSlV2QWhRa2IzMGROdzd1bXFvYkJPQ0QxRnlLdk9ISThPVGdWUDZLSUwKSEJTQ2laY2R3M3FpSWc2dE05eGMxaVY1aUEva1JjVThSSnZnSTdFdFdPcXFKNlFnZWNleCtOQU9FT0ZYOERYYgpSMXhPVmdSemR3eXNtb2IxeDJhU3UyeWRTN1NTQURaK3k0bjBJTDdNb0JtVzhnK0ZQdFFtOU8wVWl4ZllaV3lhCmVleHFOS0xLVS9neG5iZXIvQy9kVWpPS3dndmpDRHkvZjhGQ1BNcDBEZzFLdU1Uc2J5ZjRyczQvM1JkUDBtK08KdXZhTTJQaEJsNEJJQVg2NXRIc1p6TGRtZWhOdzd1RGR1eGhBenVwVkR6YlhKcGQ5cEZaWE83QzlGWXhVNFpuSgpHbnliWktQcDlrL28yVkw2OWR1d0NSdkYySlVWdEZQZ2pibG80R2o4Y1FLQmdRRFc3NXVhdGtHUmNHR1Y3QWE3CktWcXVwWXFoazlxOERvaG9CZ2xvZzZMb3REMzFxbks2b1hwM2UweS9mZElMaEFERGdCaStEYW05aGFicEV4Q3YKK29TcnVNbFJLM1EyN1ZzbFd6WVQ2K0JyZDRNS3RrNjN6TG1YS25iTWhsOE9TQ1FERmdrUXQrWExGak1FUmNmawpvb2JWem1qajdrWnh5c1hoV2xsdlFTaXkyUUtCZ1FEVkI3ZG9oNDcwZ3I2VjBvbko4VzlDazF6MTBWMEtCQ0ZjCmFkd3Z4UjBKdUhsc2ZmVVJsaU1zQ3VzQlYzWDJpb3liblNxeG14SGQ0Qm5zeWx4bFlLdEpkM2pQbE05bnVoajAKbWZwMzFIcEN6aWRZRUs5Q1RVVFBTZE5tcUlFdHJqTkppano0OHcxNWlTVFA2c2c4ZXhWVUdtTVkrUDVyeDM4SQpXSkxzU3VqdnZ3S0JnUUNrTW5QN0l4VEFHTXhVRGZXdWNZODNNSHZScC9SSUNnb20vY1dlTkVIMTZBd1ZhdHN1CnZFR2ttV3N1TnQ2SnNaUXJ4ZVlnK3FzYmY4amM4WldqK292ejY3elA1NVJtaWJsQnRvWi9mWWo2VUZpcGpGQmkKbFdHS25BUVpodVdETVpWaFRpb3F2WEl0VFk0M3kxOUR5TzJjMUl6STQ3U3BKYkU1MFIzVm9qK0hNUUtCZ0VkZwpESDJEWGN4aXVnUnN4Q25iTU5IM21kL3F3K2VGTnNCRjM3WkpyczhBOWYzNXZkQ2tveWd3aUVpc3l5Tk5qSXJlCi85ejkvZUIvSTNDSTVLZzYyV2tHRkg1SWQ2MWpWdFV0ZWhRSUp1YVhOK3R6dTZUVlNzYkJENG1IejdCRWUzNmEKU0krSXIrMFduRFRsankxa2QrTHo3RndEb1FydmpvcDNVdExFem9MMUFvR0JBTTcvWVRNWSszV1NDeENPL3NIWAo3OGZDeHhBRHFMVWMxVURYdGMzcFhKQnorL3hJeUx1Q3JQYnlsUC82L21yRjN4SENTbGg3bi9mcFovV1dRMzIxCjNyZnR5Y2czWWVzalZxdjBaZmJVb01OdFE5cGYrcFpQMGpWVEZXMlF3YTZWYURrcGdTQnB4QzlvWXlMWTRldGMKajBkWm9NeTVMYXNKcm5jUjhlTVc4NHlnCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
|
||||
type: kubernetes.io/tls
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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("<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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 <vmuzikar@redhat.com>
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue