Skip to content

Commit 878ca8b

Browse files
authored
feat(operator): Add support for properly configuring the CORS allowed origins header (#5906)
* Add support for properly configuring the CORS allowed origins header * Fix smoke test to get expected CORS allowed origins from the actual ingress * Moved CORS test examples to test/resources. Updated Cors logic.
1 parent 2095bc2 commit 878ca8b

File tree

12 files changed

+227
-18
lines changed

12 files changed

+227
-18
lines changed

operator/controller/src/main/java/io/apicurio/registry/operator/EnvironmentVariables.java

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ public class EnvironmentVariables {
1010
public static final String APICURIO_REST_DELETION_ARTIFACT_ENABLED = "APICURIO_REST_DELETION_ARTIFACT_ENABLED";
1111
public static final String APICURIO_REST_DELETION_GROUP_ENABLED = "APICURIO_REST_DELETION_GROUP_ENABLED";
1212

13+
public static final String APICURIO_REST_MUTABILITY_ARTIFACT_VERSION_CONTENT_ENABLED = "APICURIO_REST_MUTABILITY_ARTIFACT-VERSION-CONTENT_ENABLED";
14+
1315
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.apicurio.registry.operator.feat;
2+
3+
import io.apicurio.registry.operator.EnvironmentVariables;
4+
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
5+
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3Spec;
6+
import io.apicurio.registry.operator.api.v1.spec.ComponentSpec;
7+
import io.apicurio.registry.operator.resource.ResourceFactory;
8+
import io.apicurio.registry.operator.utils.IngressUtils;
9+
import io.fabric8.kubernetes.api.model.EnvVar;
10+
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
11+
12+
import java.util.Arrays;
13+
import java.util.LinkedHashMap;
14+
import java.util.Optional;
15+
import java.util.Set;
16+
import java.util.TreeSet;
17+
18+
/**
19+
* Helper class used to handle CORS related configuration.
20+
*/
21+
public class Cors {
22+
/**
23+
* Configure the QUARKUS_HTTP_CORS_ORIGINS environment variable with the following:
24+
* <ul>
25+
* <li>Add the ingress host</li>
26+
* <li>Override if QUARKUS_HTTP_CORS_ORIGINS is configured in the "env" section</li>
27+
* </ul>
28+
*
29+
* @param primary
30+
* @param envVars
31+
*/
32+
public static void configureAllowedOrigins(ApicurioRegistry3 primary,
33+
LinkedHashMap<String, EnvVar> envVars) {
34+
TreeSet<String> allowedOrigins = new TreeSet<>();
35+
36+
// If the QUARKUS_HTTP_CORS_ORIGINS env var is configured in the "env" section of the CR,
37+
// then make sure to add those configured values to the set of allowed origins we want to
38+
// configure.
39+
Optional.ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp).map(ComponentSpec::getEnv)
40+
.ifPresent(env -> {
41+
env.stream().filter(
42+
envVar -> envVar.getName().equals(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS))
43+
.forEach(envVar -> {
44+
Optional.ofNullable(envVar.getValue()).ifPresent(envVarValue -> {
45+
Arrays.stream(envVarValue.split(",")).forEach(allowedOrigins::add);
46+
});
47+
});
48+
});
49+
50+
// If not, let's try to figure it out from other sources.
51+
if (allowedOrigins.isEmpty()) {
52+
// If there is a configured Ingress host for the UI or the Studio UI, add them to the allowed
53+
// origins.
54+
Set.of(ResourceFactory.COMPONENT_UI, ResourceFactory.COMPONENT_STUDIO_UI).forEach(component -> {
55+
String host = IngressUtils.getConfiguredHost(component, primary);
56+
if (host != null) {
57+
allowedOrigins.add("http://" + host);
58+
allowedOrigins.add("https://" + host);
59+
}
60+
});
61+
}
62+
63+
// If we still do not have anything, then default to "*"
64+
if (allowedOrigins.isEmpty()) {
65+
allowedOrigins.add("*");
66+
}
67+
68+
// Join the values in allowedOrigins into a String and set it as the new value of the env var.
69+
String envVarValue = String.join(",", allowedOrigins);
70+
envVars.put(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS, new EnvVarBuilder()
71+
.withName(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS).withValue(envVarValue).build());
72+
}
73+
}

operator/controller/src/main/java/io/apicurio/registry/operator/resource/ResourceFactory.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,20 @@ public static <T> T deserialize(String path, Class<T> klass) {
274274
}
275275
}
276276

277+
public static <T> T deserialize(String path, Class<T> klass, ClassLoader classLoader) {
278+
try {
279+
return YAML_MAPPER.readValue(load(path, classLoader), klass);
280+
} catch (JsonProcessingException ex) {
281+
throw new OperatorException("Could not deserialize resource: " + path, ex);
282+
}
283+
}
284+
277285
public static String load(String path) {
278-
try (var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
286+
return load(path, Thread.currentThread().getContextClassLoader());
287+
}
288+
289+
public static String load(String path, ClassLoader classLoader) {
290+
try (var stream = classLoader.getResourceAsStream(path)) {
279291
return new String(stream.readAllBytes(), Charset.defaultCharset());
280292
} catch (Exception ex) {
281293
throw new OperatorException("Could not read resource: " + path, ex);

operator/controller/src/main/java/io/apicurio/registry/operator/resource/app/AppDeploymentResource.java

+11-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.apicurio.registry.operator.api.v1.spec.AppFeaturesSpec;
88
import io.apicurio.registry.operator.api.v1.spec.AppSpec;
99
import io.apicurio.registry.operator.api.v1.spec.StorageSpec;
10+
import io.apicurio.registry.operator.feat.Cors;
1011
import io.apicurio.registry.operator.feat.KafkaSql;
1112
import io.apicurio.registry.operator.feat.PostgresSql;
1213
import io.fabric8.kubernetes.api.model.Container;
@@ -60,7 +61,6 @@ protected Deployment desired(ApicurioRegistry3 primary, Context<ApicurioRegistry
6061
// spotless:off
6162
addEnvVar(envVars, new EnvVarBuilder().withName(EnvironmentVariables.QUARKUS_PROFILE).withValue("prod").build());
6263
addEnvVar(envVars, new EnvVarBuilder().withName(EnvironmentVariables.QUARKUS_HTTP_ACCESS_LOG_ENABLED).withValue("true").build());
63-
addEnvVar(envVars, new EnvVarBuilder().withName(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS).withValue("*").build());
6464

6565
// Enable deletes if configured in the CR
6666
boolean allowDeletes = Optional.ofNullable(primary.getSpec().getApp())
@@ -72,18 +72,23 @@ protected Deployment desired(ApicurioRegistry3 primary, Context<ApicurioRegistry
7272
addEnvVar(envVars, new EnvVarBuilder().withName(EnvironmentVariables.APICURIO_REST_DELETION_ARTIFACT_ENABLED).withValue("true").build());
7373
addEnvVar(envVars, new EnvVarBuilder().withName(EnvironmentVariables.APICURIO_REST_DELETION_GROUP_ENABLED).withValue("true").build());
7474
}
75-
// spotless:on
7675

77-
// This is enabled only if Studio is deployed. It is based on Service in case a custom Ingress is
78-
// used.
76+
// Configure the CORS_ALLOWED_ORIGINS env var based on the ingress host
77+
Cors.configureAllowedOrigins(primary, envVars);
78+
79+
// Enable the "mutability" feature in Registry, but only if Studio is deployed. It is based on Service
80+
// in case a custom Ingress is used.
7981
var sOpt = context.getSecondaryResource(STUDIO_UI_SERVICE_KEY.getKlass(),
8082
STUDIO_UI_SERVICE_KEY.getDiscriminator());
8183
sOpt.ifPresent(s -> {
8284
addEnvVar(envVars,
83-
new EnvVarBuilder().withName("APICURIO_REST_MUTABILITY_ARTIFACT-VERSION-CONTENT_ENABLED")
85+
new EnvVarBuilder().withName(EnvironmentVariables.APICURIO_REST_MUTABILITY_ARTIFACT_VERSION_CONTENT_ENABLED)
8486
.withValue("true").build());
8587
});
8688

89+
// spotless:on
90+
91+
// Configure the storage (Postgresql or KafkaSql).
8792
ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp).map(AppSpec::getStorage)
8893
.map(StorageSpec::getType).ifPresent(storageType -> {
8994
switch (storageType) {
@@ -92,6 +97,7 @@ protected Deployment desired(ApicurioRegistry3 primary, Context<ApicurioRegistry
9297
}
9398
});
9499

100+
// Set the ENV VARs on the deployment's container spec.
95101
var container = getContainerFromDeployment(d, REGISTRY_APP_CONTAINER_NAME);
96102
container.setEnv(envVars.values().stream().toList());
97103

operator/controller/src/main/java/io/apicurio/registry/operator/utils/IngressUtils.java

+26-8
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,39 @@ public final class IngressUtils {
2828
private IngressUtils() {
2929
}
3030

31-
public static String getHost(String component, ApicurioRegistry3 p) {
31+
/**
32+
* Get the host configured in the ingress. If no host is configured in the ingress then a null is
33+
* returned.
34+
*
35+
* @param component
36+
* @param primary
37+
*/
38+
public static String getConfiguredHost(String component, ApicurioRegistry3 primary) {
3239
String host = switch (component) {
33-
case COMPONENT_APP -> ofNullable(p.getSpec()).map(ApicurioRegistry3Spec::getApp)
40+
case COMPONENT_APP -> ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp)
3441
.map(AppSpec::getIngress).map(IngressSpec::getHost).filter(h -> !isBlank(h)).orElse(null);
35-
case COMPONENT_UI -> ofNullable(p.getSpec()).map(ApicurioRegistry3Spec::getUi)
42+
case COMPONENT_UI -> ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getUi)
3643
.map(UiSpec::getIngress).map(IngressSpec::getHost).filter(h -> !isBlank(h)).orElse(null);
37-
case COMPONENT_STUDIO_UI ->
38-
ofNullable(p.getSpec()).map(ApicurioRegistry3Spec::getStudioUi).map(StudioUiSpec::getIngress)
39-
.map(IngressSpec::getHost).filter(h -> !isBlank(h)).orElse(null);
44+
case COMPONENT_STUDIO_UI -> ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getStudioUi)
45+
.map(StudioUiSpec::getIngress).map(IngressSpec::getHost).filter(h -> !isBlank(h))
46+
.orElse(null);
4047
default -> throw new OperatorException("Unexpected value: " + component);
4148
};
49+
return host;
50+
}
51+
52+
/**
53+
* Get the host for an ingress. If not configured, a default value is returned.
54+
*
55+
* @param component
56+
* @param primary
57+
*/
58+
public static String getHost(String component, ApicurioRegistry3 primary) {
59+
String host = getConfiguredHost(component, primary);
4260
if (host == null) {
4361
// TODO: This is not used because of the current activation conditions.
44-
host = "%s-%s.%s%s".formatted(p.getMetadata().getName(), component,
45-
p.getMetadata().getNamespace(), Configuration.getDefaultBaseHost());
62+
host = "%s-%s.%s%s".formatted(primary.getMetadata().getName(), component,
63+
primary.getMetadata().getNamespace(), Configuration.getDefaultBaseHost());
4664
}
4765
log.debug("Host for component {} is {}", component, host);
4866
return host;

operator/controller/src/test/java/io/apicurio/registry/operator/it/AppFeaturesITTest.java

-4
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import io.fabric8.kubernetes.api.model.EnvVar;
88
import io.quarkus.test.junit.QuarkusTest;
99
import org.junit.jupiter.api.Test;
10-
import org.slf4j.Logger;
11-
import org.slf4j.LoggerFactory;
1210

1311
import static io.apicurio.registry.operator.api.v1.ContainerNames.REGISTRY_APP_CONTAINER_NAME;
1412
import static io.apicurio.registry.operator.resource.app.AppDeploymentResource.getContainerFromDeployment;
@@ -17,8 +15,6 @@
1715
@QuarkusTest
1816
public class AppFeaturesITTest extends ITBase {
1917

20-
private static final Logger log = LoggerFactory.getLogger(AppFeaturesITTest.class);
21-
2218
@Test
2319
void testAllowDeletesTrue() {
2420
ApicurioRegistry3 registry = ResourceFactory

operator/controller/src/test/java/io/apicurio/registry/operator/it/SmokeITTest.java

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.apicurio.registry.operator.it;
22

3+
import io.apicurio.registry.operator.EnvironmentVariables;
34
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
45
import io.apicurio.registry.operator.resource.ResourceFactory;
56
import io.fabric8.kubernetes.api.model.Container;
@@ -15,9 +16,11 @@
1516

1617
import java.net.URI;
1718

19+
import static io.apicurio.registry.operator.api.v1.ContainerNames.REGISTRY_APP_CONTAINER_NAME;
1820
import static io.apicurio.registry.operator.api.v1.ContainerNames.REGISTRY_UI_CONTAINER_NAME;
1921
import static io.apicurio.registry.operator.resource.ResourceFactory.COMPONENT_APP;
2022
import static io.apicurio.registry.operator.resource.ResourceFactory.COMPONENT_UI;
23+
import static io.apicurio.registry.operator.resource.app.AppDeploymentResource.getContainerFromDeployment;
2124
import static io.restassured.RestAssured.given;
2225
import static org.assertj.core.api.Assertions.assertThat;
2326
import static org.awaitility.Awaitility.await;
@@ -70,6 +73,18 @@ void smoke() {
7073
.get(0).getHost()).isEqualTo(registry.getSpec().getUi().getIngress().getHost());
7174
return true;
7275
});
76+
77+
// Check CORS allowed origins is set on the app, with the value based on the UI ingress host
78+
String uiIngressHost = client.network().v1().ingresses().inNamespace(namespace)
79+
.withName(registry.getMetadata().getName() + "-ui-ingress").get().getSpec().getRules().get(0)
80+
.getHost();
81+
String corsOriginsExpectedValue = "http://" + uiIngressHost + "," + "https://" + uiIngressHost;
82+
var appEnv = getContainerFromDeployment(
83+
client.apps().deployments().inNamespace(namespace)
84+
.withName(registry.getMetadata().getName() + "-app-deployment").get(),
85+
REGISTRY_APP_CONTAINER_NAME).getEnv();
86+
assertThat(appEnv).map(ev -> ev.getName() + "=" + ev.getValue())
87+
.contains(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS + "=" + corsOriginsExpectedValue);
7388
}
7489

7590
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.apicurio.registry.operator.unit;
2+
3+
import io.apicurio.registry.operator.EnvironmentVariables;
4+
import io.apicurio.registry.operator.OperatorException;
5+
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
6+
import io.apicurio.registry.operator.feat.Cors;
7+
import io.apicurio.registry.operator.resource.ResourceFactory;
8+
import io.fabric8.kubernetes.api.model.EnvVar;
9+
import org.assertj.core.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.nio.charset.Charset;
13+
import java.util.LinkedHashMap;
14+
import java.util.Set;
15+
16+
public class CorsTest {
17+
18+
@Test
19+
public void testConfigureAllowedOrigins() throws Exception {
20+
doTestAllowedOrigins("k8s/examples/cors/example-default.yaml", "*");
21+
doTestAllowedOrigins("k8s/examples/cors/example-ingress.yaml",
22+
"http://simple-ui.apps.cluster.example", "https://simple-ui.apps.cluster.example");
23+
doTestAllowedOrigins("k8s/examples/cors/example-env-vars.yaml", "https://ui.example.org");
24+
doTestAllowedOrigins("k8s/examples/cors/example-env-vars-and-ingress.yaml", "https://ui.example.org");
25+
}
26+
27+
private void doTestAllowedOrigins(String crPath, String... values) {
28+
ClassLoader classLoader = CorsTest.class.getClassLoader();
29+
ApicurioRegistry3 registry = ResourceFactory.deserialize(crPath, ApicurioRegistry3.class,
30+
classLoader);
31+
32+
LinkedHashMap<String, EnvVar> envVars = new LinkedHashMap<>();
33+
Cors.configureAllowedOrigins(registry, envVars);
34+
Assertions.assertThat(envVars.keySet()).contains(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS);
35+
String allowedOriginsValue = envVars.get(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS).getValue();
36+
Set<String> allowedOrigins = Set.of(allowedOriginsValue.split(","));
37+
Assertions.assertThat(allowedOrigins).containsExactlyInAnyOrder(values);
38+
}
39+
40+
public static String load(String path) {
41+
try (var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
42+
return new String(stream.readAllBytes(), Charset.defaultCharset());
43+
} catch (Exception ex) {
44+
throw new OperatorException("Could not read resource: " + path, ex);
45+
}
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: registry.apicur.io/v1
2+
kind: ApicurioRegistry3
3+
metadata:
4+
name: simple
5+
spec: {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: registry.apicur.io/v1
2+
kind: ApicurioRegistry3
3+
metadata:
4+
name: simple
5+
spec:
6+
app:
7+
ingress:
8+
host: simple-app.apps.cluster.example
9+
env:
10+
- name: QUARKUS_HTTP_CORS_ORIGINS
11+
value: https://ui.example.org
12+
ui:
13+
ingress:
14+
host: simple-ui.apps.cluster.example
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: registry.apicur.io/v1
2+
kind: ApicurioRegistry3
3+
metadata:
4+
name: simple
5+
spec:
6+
app:
7+
env:
8+
- name: QUARKUS_HTTP_CORS_ORIGINS
9+
value: https://ui.example.org
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: registry.apicur.io/v1
2+
kind: ApicurioRegistry3
3+
metadata:
4+
name: simple
5+
spec:
6+
app:
7+
ingress:
8+
host: simple-app.apps.cluster.example
9+
ui:
10+
ingress:
11+
host: simple-ui.apps.cluster.example

0 commit comments

Comments
 (0)