diff --git a/operator/controller/src/main/java/io/apicurio/registry/operator/EnvironmentVariables.java b/operator/controller/src/main/java/io/apicurio/registry/operator/EnvironmentVariables.java
index a97a5961a0..3d72573cf4 100644
--- a/operator/controller/src/main/java/io/apicurio/registry/operator/EnvironmentVariables.java
+++ b/operator/controller/src/main/java/io/apicurio/registry/operator/EnvironmentVariables.java
@@ -10,4 +10,6 @@ public class EnvironmentVariables {
public static final String APICURIO_REST_DELETION_ARTIFACT_ENABLED = "APICURIO_REST_DELETION_ARTIFACT_ENABLED";
public static final String APICURIO_REST_DELETION_GROUP_ENABLED = "APICURIO_REST_DELETION_GROUP_ENABLED";
+ public static final String APICURIO_REST_MUTABILITY_ARTIFACT_VERSION_CONTENT_ENABLED = "APICURIO_REST_MUTABILITY_ARTIFACT-VERSION-CONTENT_ENABLED";
+
}
diff --git a/operator/controller/src/main/java/io/apicurio/registry/operator/feat/Cors.java b/operator/controller/src/main/java/io/apicurio/registry/operator/feat/Cors.java
new file mode 100644
index 0000000000..21397369fe
--- /dev/null
+++ b/operator/controller/src/main/java/io/apicurio/registry/operator/feat/Cors.java
@@ -0,0 +1,73 @@
+package io.apicurio.registry.operator.feat;
+
+import io.apicurio.registry.operator.EnvironmentVariables;
+import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
+import io.apicurio.registry.operator.api.v1.ApicurioRegistry3Spec;
+import io.apicurio.registry.operator.api.v1.spec.ComponentSpec;
+import io.apicurio.registry.operator.resource.ResourceFactory;
+import io.apicurio.registry.operator.utils.IngressUtils;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.EnvVarBuilder;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Helper class used to handle CORS related configuration.
+ */
+public class Cors {
+ /**
+ * Configure the QUARKUS_HTTP_CORS_ORIGINS environment variable with the following:
+ *
+ * - Add the ingress host
+ * - Override if QUARKUS_HTTP_CORS_ORIGINS is configured in the "env" section
+ *
+ *
+ * @param primary
+ * @param envVars
+ */
+ public static void configureAllowedOrigins(ApicurioRegistry3 primary,
+ LinkedHashMap envVars) {
+ TreeSet allowedOrigins = new TreeSet<>();
+
+ // If the QUARKUS_HTTP_CORS_ORIGINS env var is configured in the "env" section of the CR,
+ // then make sure to add those configured values to the set of allowed origins we want to
+ // configure.
+ Optional.ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp).map(ComponentSpec::getEnv)
+ .ifPresent(env -> {
+ env.stream().filter(
+ envVar -> envVar.getName().equals(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS))
+ .forEach(envVar -> {
+ Optional.ofNullable(envVar.getValue()).ifPresent(envVarValue -> {
+ Arrays.stream(envVarValue.split(",")).forEach(allowedOrigins::add);
+ });
+ });
+ });
+
+ // If not, let's try to figure it out from other sources.
+ if (allowedOrigins.isEmpty()) {
+ // If there is a configured Ingress host for the UI or the Studio UI, add them to the allowed
+ // origins.
+ Set.of(ResourceFactory.COMPONENT_UI, ResourceFactory.COMPONENT_STUDIO_UI).forEach(component -> {
+ String host = IngressUtils.getConfiguredHost(component, primary);
+ if (host != null) {
+ allowedOrigins.add("http://" + host);
+ allowedOrigins.add("https://" + host);
+ }
+ });
+ }
+
+ // If we still do not have anything, then default to "*"
+ if (allowedOrigins.isEmpty()) {
+ allowedOrigins.add("*");
+ }
+
+ // Join the values in allowedOrigins into a String and set it as the new value of the env var.
+ String envVarValue = String.join(",", allowedOrigins);
+ envVars.put(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS, new EnvVarBuilder()
+ .withName(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS).withValue(envVarValue).build());
+ }
+}
diff --git a/operator/controller/src/main/java/io/apicurio/registry/operator/resource/ResourceFactory.java b/operator/controller/src/main/java/io/apicurio/registry/operator/resource/ResourceFactory.java
index 9f232cf525..dd65faaab2 100644
--- a/operator/controller/src/main/java/io/apicurio/registry/operator/resource/ResourceFactory.java
+++ b/operator/controller/src/main/java/io/apicurio/registry/operator/resource/ResourceFactory.java
@@ -274,8 +274,20 @@ public static T deserialize(String path, Class klass) {
}
}
+ public static T deserialize(String path, Class klass, ClassLoader classLoader) {
+ try {
+ return YAML_MAPPER.readValue(load(path, classLoader), klass);
+ } catch (JsonProcessingException ex) {
+ throw new OperatorException("Could not deserialize resource: " + path, ex);
+ }
+ }
+
public static String load(String path) {
- try (var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
+ return load(path, Thread.currentThread().getContextClassLoader());
+ }
+
+ public static String load(String path, ClassLoader classLoader) {
+ try (var stream = classLoader.getResourceAsStream(path)) {
return new String(stream.readAllBytes(), Charset.defaultCharset());
} catch (Exception ex) {
throw new OperatorException("Could not read resource: " + path, ex);
diff --git a/operator/controller/src/main/java/io/apicurio/registry/operator/resource/app/AppDeploymentResource.java b/operator/controller/src/main/java/io/apicurio/registry/operator/resource/app/AppDeploymentResource.java
index 00361bd198..8eeba252e6 100644
--- a/operator/controller/src/main/java/io/apicurio/registry/operator/resource/app/AppDeploymentResource.java
+++ b/operator/controller/src/main/java/io/apicurio/registry/operator/resource/app/AppDeploymentResource.java
@@ -7,6 +7,7 @@
import io.apicurio.registry.operator.api.v1.spec.AppFeaturesSpec;
import io.apicurio.registry.operator.api.v1.spec.AppSpec;
import io.apicurio.registry.operator.api.v1.spec.StorageSpec;
+import io.apicurio.registry.operator.feat.Cors;
import io.apicurio.registry.operator.feat.KafkaSql;
import io.apicurio.registry.operator.feat.PostgresSql;
import io.fabric8.kubernetes.api.model.Container;
@@ -60,7 +61,6 @@ protected Deployment desired(ApicurioRegistry3 primary, Context {
addEnvVar(envVars,
- new EnvVarBuilder().withName("APICURIO_REST_MUTABILITY_ARTIFACT-VERSION-CONTENT_ENABLED")
+ new EnvVarBuilder().withName(EnvironmentVariables.APICURIO_REST_MUTABILITY_ARTIFACT_VERSION_CONTENT_ENABLED)
.withValue("true").build());
});
+ // spotless:on
+
+ // Configure the storage (Postgresql or KafkaSql).
ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp).map(AppSpec::getStorage)
.map(StorageSpec::getType).ifPresent(storageType -> {
switch (storageType) {
@@ -92,6 +97,7 @@ protected Deployment desired(ApicurioRegistry3 primary, Context ofNullable(p.getSpec()).map(ApicurioRegistry3Spec::getApp)
+ case COMPONENT_APP -> ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp)
.map(AppSpec::getIngress).map(IngressSpec::getHost).filter(h -> !isBlank(h)).orElse(null);
- case COMPONENT_UI -> ofNullable(p.getSpec()).map(ApicurioRegistry3Spec::getUi)
+ case COMPONENT_UI -> ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getUi)
.map(UiSpec::getIngress).map(IngressSpec::getHost).filter(h -> !isBlank(h)).orElse(null);
- case COMPONENT_STUDIO_UI ->
- ofNullable(p.getSpec()).map(ApicurioRegistry3Spec::getStudioUi).map(StudioUiSpec::getIngress)
- .map(IngressSpec::getHost).filter(h -> !isBlank(h)).orElse(null);
+ case COMPONENT_STUDIO_UI -> ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getStudioUi)
+ .map(StudioUiSpec::getIngress).map(IngressSpec::getHost).filter(h -> !isBlank(h))
+ .orElse(null);
default -> throw new OperatorException("Unexpected value: " + component);
};
+ return host;
+ }
+
+ /**
+ * Get the host for an ingress. If not configured, a default value is returned.
+ *
+ * @param component
+ * @param primary
+ */
+ public static String getHost(String component, ApicurioRegistry3 primary) {
+ String host = getConfiguredHost(component, primary);
if (host == null) {
// TODO: This is not used because of the current activation conditions.
- host = "%s-%s.%s%s".formatted(p.getMetadata().getName(), component,
- p.getMetadata().getNamespace(), Configuration.getDefaultBaseHost());
+ host = "%s-%s.%s%s".formatted(primary.getMetadata().getName(), component,
+ primary.getMetadata().getNamespace(), Configuration.getDefaultBaseHost());
}
log.debug("Host for component {} is {}", component, host);
return host;
diff --git a/operator/controller/src/test/java/io/apicurio/registry/operator/it/AppFeaturesITTest.java b/operator/controller/src/test/java/io/apicurio/registry/operator/it/AppFeaturesITTest.java
index 2420afebc2..aaadc08dc8 100644
--- a/operator/controller/src/test/java/io/apicurio/registry/operator/it/AppFeaturesITTest.java
+++ b/operator/controller/src/test/java/io/apicurio/registry/operator/it/AppFeaturesITTest.java
@@ -7,8 +7,6 @@
import io.fabric8.kubernetes.api.model.EnvVar;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import static io.apicurio.registry.operator.api.v1.ContainerNames.REGISTRY_APP_CONTAINER_NAME;
import static io.apicurio.registry.operator.resource.app.AppDeploymentResource.getContainerFromDeployment;
@@ -17,8 +15,6 @@
@QuarkusTest
public class AppFeaturesITTest extends ITBase {
- private static final Logger log = LoggerFactory.getLogger(AppFeaturesITTest.class);
-
@Test
void testAllowDeletesTrue() {
ApicurioRegistry3 registry = ResourceFactory
diff --git a/operator/controller/src/test/java/io/apicurio/registry/operator/it/SmokeITTest.java b/operator/controller/src/test/java/io/apicurio/registry/operator/it/SmokeITTest.java
index eb8a51a074..33d69ec956 100644
--- a/operator/controller/src/test/java/io/apicurio/registry/operator/it/SmokeITTest.java
+++ b/operator/controller/src/test/java/io/apicurio/registry/operator/it/SmokeITTest.java
@@ -1,5 +1,6 @@
package io.apicurio.registry.operator.it;
+import io.apicurio.registry.operator.EnvironmentVariables;
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
import io.apicurio.registry.operator.resource.ResourceFactory;
import io.fabric8.kubernetes.api.model.Container;
@@ -15,9 +16,11 @@
import java.net.URI;
+import static io.apicurio.registry.operator.api.v1.ContainerNames.REGISTRY_APP_CONTAINER_NAME;
import static io.apicurio.registry.operator.api.v1.ContainerNames.REGISTRY_UI_CONTAINER_NAME;
import static io.apicurio.registry.operator.resource.ResourceFactory.COMPONENT_APP;
import static io.apicurio.registry.operator.resource.ResourceFactory.COMPONENT_UI;
+import static io.apicurio.registry.operator.resource.app.AppDeploymentResource.getContainerFromDeployment;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
@@ -70,6 +73,18 @@ void smoke() {
.get(0).getHost()).isEqualTo(registry.getSpec().getUi().getIngress().getHost());
return true;
});
+
+ // Check CORS allowed origins is set on the app, with the value based on the UI ingress host
+ String uiIngressHost = client.network().v1().ingresses().inNamespace(namespace)
+ .withName(registry.getMetadata().getName() + "-ui-ingress").get().getSpec().getRules().get(0)
+ .getHost();
+ String corsOriginsExpectedValue = "http://" + uiIngressHost + "," + "https://" + uiIngressHost;
+ var appEnv = getContainerFromDeployment(
+ client.apps().deployments().inNamespace(namespace)
+ .withName(registry.getMetadata().getName() + "-app-deployment").get(),
+ REGISTRY_APP_CONTAINER_NAME).getEnv();
+ assertThat(appEnv).map(ev -> ev.getName() + "=" + ev.getValue())
+ .contains(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS + "=" + corsOriginsExpectedValue);
}
@Test
diff --git a/operator/controller/src/test/java/io/apicurio/registry/operator/unit/CorsTest.java b/operator/controller/src/test/java/io/apicurio/registry/operator/unit/CorsTest.java
new file mode 100644
index 0000000000..cd6d203cdc
--- /dev/null
+++ b/operator/controller/src/test/java/io/apicurio/registry/operator/unit/CorsTest.java
@@ -0,0 +1,48 @@
+package io.apicurio.registry.operator.unit;
+
+import io.apicurio.registry.operator.EnvironmentVariables;
+import io.apicurio.registry.operator.OperatorException;
+import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
+import io.apicurio.registry.operator.feat.Cors;
+import io.apicurio.registry.operator.resource.ResourceFactory;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.Charset;
+import java.util.LinkedHashMap;
+import java.util.Set;
+
+public class CorsTest {
+
+ @Test
+ public void testConfigureAllowedOrigins() throws Exception {
+ doTestAllowedOrigins("k8s/examples/cors/example-default.yaml", "*");
+ doTestAllowedOrigins("k8s/examples/cors/example-ingress.yaml",
+ "http://simple-ui.apps.cluster.example", "https://simple-ui.apps.cluster.example");
+ doTestAllowedOrigins("k8s/examples/cors/example-env-vars.yaml", "https://ui.example.org");
+ doTestAllowedOrigins("k8s/examples/cors/example-env-vars-and-ingress.yaml", "https://ui.example.org");
+ }
+
+ private void doTestAllowedOrigins(String crPath, String... values) {
+ ClassLoader classLoader = CorsTest.class.getClassLoader();
+ ApicurioRegistry3 registry = ResourceFactory.deserialize(crPath, ApicurioRegistry3.class,
+ classLoader);
+
+ LinkedHashMap envVars = new LinkedHashMap<>();
+ Cors.configureAllowedOrigins(registry, envVars);
+ Assertions.assertThat(envVars.keySet()).contains(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS);
+ String allowedOriginsValue = envVars.get(EnvironmentVariables.QUARKUS_HTTP_CORS_ORIGINS).getValue();
+ Set allowedOrigins = Set.of(allowedOriginsValue.split(","));
+ Assertions.assertThat(allowedOrigins).containsExactlyInAnyOrder(values);
+ }
+
+ public static String load(String path) {
+ try (var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
+ return new String(stream.readAllBytes(), Charset.defaultCharset());
+ } catch (Exception ex) {
+ throw new OperatorException("Could not read resource: " + path, ex);
+ }
+ }
+
+}
diff --git a/operator/controller/src/test/resources/k8s/examples/cors/example-default.yaml b/operator/controller/src/test/resources/k8s/examples/cors/example-default.yaml
new file mode 100644
index 0000000000..53e6d43ef7
--- /dev/null
+++ b/operator/controller/src/test/resources/k8s/examples/cors/example-default.yaml
@@ -0,0 +1,5 @@
+apiVersion: registry.apicur.io/v1
+kind: ApicurioRegistry3
+metadata:
+ name: simple
+spec: {}
diff --git a/operator/controller/src/test/resources/k8s/examples/cors/example-env-vars-and-ingress.yaml b/operator/controller/src/test/resources/k8s/examples/cors/example-env-vars-and-ingress.yaml
new file mode 100644
index 0000000000..10a0a16c8a
--- /dev/null
+++ b/operator/controller/src/test/resources/k8s/examples/cors/example-env-vars-and-ingress.yaml
@@ -0,0 +1,14 @@
+apiVersion: registry.apicur.io/v1
+kind: ApicurioRegistry3
+metadata:
+ name: simple
+spec:
+ app:
+ ingress:
+ host: simple-app.apps.cluster.example
+ env:
+ - name: QUARKUS_HTTP_CORS_ORIGINS
+ value: https://ui.example.org
+ ui:
+ ingress:
+ host: simple-ui.apps.cluster.example
diff --git a/operator/controller/src/test/resources/k8s/examples/cors/example-env-vars.yaml b/operator/controller/src/test/resources/k8s/examples/cors/example-env-vars.yaml
new file mode 100644
index 0000000000..9525aee1ac
--- /dev/null
+++ b/operator/controller/src/test/resources/k8s/examples/cors/example-env-vars.yaml
@@ -0,0 +1,9 @@
+apiVersion: registry.apicur.io/v1
+kind: ApicurioRegistry3
+metadata:
+ name: simple
+spec:
+ app:
+ env:
+ - name: QUARKUS_HTTP_CORS_ORIGINS
+ value: https://ui.example.org
diff --git a/operator/controller/src/test/resources/k8s/examples/cors/example-ingress.yaml b/operator/controller/src/test/resources/k8s/examples/cors/example-ingress.yaml
new file mode 100644
index 0000000000..2cd8be6262
--- /dev/null
+++ b/operator/controller/src/test/resources/k8s/examples/cors/example-ingress.yaml
@@ -0,0 +1,11 @@
+apiVersion: registry.apicur.io/v1
+kind: ApicurioRegistry3
+metadata:
+ name: simple
+spec:
+ app:
+ ingress:
+ host: simple-app.apps.cluster.example
+ ui:
+ ingress:
+ host: simple-ui.apps.cluster.example