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_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";
}

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() {
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());

View file

@ -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);

View file

@ -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)

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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

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.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");

View file

@ -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;
}
}
}

View file

@ -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

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.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);

View file

@ -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);
}

View file

@ -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:

View file

@ -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: