From 5e44b81ddb0285dde6f9c31418e71af005b0c79f Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 10 Mar 2023 08:59:22 +0100 Subject: [PATCH] Allow configuring the deploy strategy to perform K8s deployments Fix https://github.com/quarkusio/quarkus/issues/26789 --- .../deployment/OpenshiftProcessor.java | 76 ++++++++---- .../openshift/deployment/OpenshiftUtils.java | 12 ++ .../kind/deployment/KindProcessor.java | 2 +- .../deployment/MinikubeProcessor.java | 2 +- .../deployment/OpenshiftProcessor.java | 3 +- .../kubernetes/spi/DeployStrategy.java | 21 ++++ .../KubernetesDeploymentTargetBuildItem.java | 21 +++- ...bernetesDeploymentTargetBuildItemTest.java | 25 ++-- .../deployment/DeploymentTargetEntry.java | 10 +- .../kubernetes/deployment/KnativeConfig.java | 7 ++ .../deployment/KnativeProcessor.java | 3 +- .../deployment/KubernetesConfig.java | 11 ++ .../deployment/KubernetesDeployer.java | 108 ++++++++++++------ .../deployment/KubernetesProcessor.java | 3 +- .../deployment/OpenshiftConfig.java | 11 ++ .../deployment/OpenshiftProcessor.java | 2 +- .../VanillaKubernetesProcessor.java | 6 +- 17 files changed, 236 insertions(+), 87 deletions(-) create mode 100644 extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/DeployStrategy.java diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java index f6d78bd7e166a..b81e54626c14a 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.container.image.openshift.deployment; +import static io.quarkus.container.image.openshift.deployment.OpenshiftUtils.getDeployStrategy; import static io.quarkus.container.image.openshift.deployment.OpenshiftUtils.getNamespace; import static io.quarkus.container.image.openshift.deployment.OpenshiftUtils.mergeConfig; import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; @@ -39,6 +40,9 @@ import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.LogWatch; +import io.fabric8.kubernetes.client.dsl.NamespaceableResource; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.fabric8.kubernetes.client.http.HttpClient; import io.fabric8.openshift.api.model.Build; import io.fabric8.openshift.api.model.BuildConfig; @@ -68,6 +72,7 @@ import io.quarkus.kubernetes.client.deployment.KubernetesClientErrorHandler; import io.quarkus.kubernetes.client.spi.KubernetesClientBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; +import io.quarkus.kubernetes.spi.DeployStrategy; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -403,27 +408,7 @@ private static void applyOpenshiftResources(OpenShiftClient client, List d == null, 10, TimeUnit.SECONDS); - } catch (IllegalArgumentException e) { - // We should ignore that, as its expected to be thrown when item is actually - // deleted. - } - } else if (i instanceof ImageStream) { - ImageStream is = (ImageStream) i; - ImageStream existing = client.imageStreams().withName(i.getMetadata().getName()).get(); - if (existing != null && - existing.getSpec() != null && - existing.getSpec().getDockerImageRepository() != null && - existing.getSpec().getDockerImageRepository().equals(is.getSpec().getDockerImageRepository())) { - LOG.info("Found: " + i.getKind() + " " + i.getMetadata().getName() + " repository: " - + existing.getSpec().getDockerImageRepository()); - continue; - } - } - client.resource(i).createOrReplace(); + deployResource(client, i); LOG.info("Applied: " + i.getKind() + " " + i.getMetadata().getName()); } OpenshiftUtils.waitForImageStreamTags(client, buildResources, 2, TimeUnit.MINUTES); @@ -549,6 +534,55 @@ private static KubernetesClient buildClient(KubernetesClientBuildItem kubernetes return kubernetesClientBuilder.buildClient(); } + private static void deployResource(OpenShiftClient client, HasMetadata metadata) { + DeployStrategy deployStrategy = getDeployStrategy(); + var r = client.resource(metadata); + // Delete build config it already existed unless the deploy strategy is not create or update. + if (deployStrategy != DeployStrategy.CreateOrUpdate && r instanceof BuildConfig) { + deleteBuildConfig(client, metadata, r); + } + + // If the image stream is already installed, we proceed with the next. + if (r instanceof ImageStream) { + ImageStream is = (ImageStream) r; + ImageStream existing = client.imageStreams().withName(metadata.getMetadata().getName()).get(); + if (existing != null && + existing.getSpec() != null && + existing.getSpec().getDockerImageRepository() != null && + existing.getSpec().getDockerImageRepository().equals(is.getSpec().getDockerImageRepository())) { + LOG.info("Found: " + metadata.getKind() + " " + metadata.getMetadata().getName() + " repository: " + + existing.getSpec().getDockerImageRepository()); + return; + } + } + + // Deploy the current resource. + switch (deployStrategy) { + case Create: + r.create(); + break; + case Replace: + r.replace(); + break; + case ServerSideApply: + r.patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY)); + break; + default: + r.createOrReplace(); + break; + } + } + + private static void deleteBuildConfig(OpenShiftClient client, HasMetadata metadata, NamespaceableResource r) { + r.cascading(true).delete(); + try { + client.resource(metadata).waitUntilCondition(d -> d == null, 10, TimeUnit.SECONDS); + } catch (IllegalArgumentException e) { + // We should ignore that, as its expected to be thrown when item is actually + // deleted. + } + } + // visible for test static String concatUnixPaths(String... elements) { StringBuilder result = new StringBuilder(); diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftUtils.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftUtils.java index 461c63097c1ee..e2bdb427a915c 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftUtils.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftUtils.java @@ -19,6 +19,7 @@ import io.fabric8.openshift.api.model.ImageStreamTag; import io.fabric8.openshift.api.model.SourceBuildStrategyFluent; import io.fabric8.openshift.client.OpenShiftClient; +import io.quarkus.kubernetes.spi.DeployStrategy; /** * This class is copied from Dekorate, with the difference that the {@code waitForImageStreamTags} method @@ -30,6 +31,8 @@ public class OpenshiftUtils { private static final String OPENSHIFT_NAMESPACE = "quarkus.openshift.namespace"; private static final String KUBERNETES_NAMESPACE = "quarkus.kubernetes.namespace"; + private static final String OPENSHIFT_DEPLOY_STRATEGY = "quarkus.openshift.deploy-strategy"; + private static final String KUBERNETES_DEPLOY_STRATEGY = "quarkus.kubernetes.deploy-strategy"; /** * Wait for the references ImageStreamTags to become available. @@ -153,4 +156,13 @@ public static Optional getNamespace() { return ConfigProvider.getConfig().getOptionalValue(OPENSHIFT_NAMESPACE, String.class) .or(() -> ConfigProvider.getConfig().getOptionalValue(KUBERNETES_NAMESPACE, String.class)); } + + /** + * @return the openshift deploy strategy set in the OpenShift/Kubernetes extensions. + */ + public static DeployStrategy getDeployStrategy() { + return ConfigProvider.getConfig().getOptionalValue(OPENSHIFT_DEPLOY_STRATEGY, DeployStrategy.class) + .or(() -> ConfigProvider.getConfig().getOptionalValue(KUBERNETES_DEPLOY_STRATEGY, DeployStrategy.class)) + .orElse(DeployStrategy.CreateOrUpdate); + } } diff --git a/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java b/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java index d3ff1e49e8b4f..2c7ccf92ebcb4 100644 --- a/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java +++ b/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java @@ -57,7 +57,7 @@ public void checkKind(ApplicationInfoBuildItem applicationInfo, KubernetesConfig BuildProducer resourceMeta) { deploymentTargets.produce( new KubernetesDeploymentTargetBuildItem(KIND, DEPLOYMENT, DEPLOYMENT_GROUP, DEPLOYMENT_VERSION, - KIND_PRIORITY, true)); + KIND_PRIORITY, true, config.getDeployStrategy())); String name = ResourceNameUtil.getResourceName(config, applicationInfo); resourceMeta.produce( diff --git a/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java b/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java index 299079fa9a47e..ad8e89775d705 100644 --- a/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java +++ b/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java @@ -54,7 +54,7 @@ public void checkMinikube(ApplicationInfoBuildItem applicationInfo, KubernetesCo BuildProducer resourceMeta) { deploymentTargets.produce( new KubernetesDeploymentTargetBuildItem(MINIKUBE, DEPLOYMENT, DEPLOYMENT_GROUP, DEPLOYMENT_VERSION, - MINIKUBE_PRIORITY, true)); + MINIKUBE_PRIORITY, true, config.getDeployStrategy())); String name = ResourceNameUtil.getResourceName(config, applicationInfo); resourceMeta.produce( diff --git a/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java b/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java index 5644cc63da4ec..6460ca8c0feef 100644 --- a/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/openshift/deployment/src/main/java/io/quarkus/openshift/deployment/OpenshiftProcessor.java @@ -24,7 +24,8 @@ public void checkOpenshift(ApplicationInfoBuildItem applicationInfo, Capabilitie .produce( new KubernetesDeploymentTargetBuildItem(OPENSHIFT, deploymentResourceKind.kind, deploymentResourceKind.apiGroup, - deploymentResourceKind.apiVersion, true)); + deploymentResourceKind.apiVersion, true, + config.getDeployStrategy())); String name = ResourceNameUtil.getResourceName(config, applicationInfo); resourceMeta.produce(new KubernetesResourceMetadataBuildItem(OPENSHIFT, deploymentResourceKind.apiGroup, diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/DeployStrategy.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/DeployStrategy.java new file mode 100644 index 0000000000000..77cae719c39f9 --- /dev/null +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/DeployStrategy.java @@ -0,0 +1,21 @@ +package io.quarkus.kubernetes.spi; + +public enum DeployStrategy { + /** + * To create or replace the resources. + */ + CreateOrUpdate, + /** + * To create the resources if it does not exist. If resources already exist, it will fail. + */ + Create, + /** + * To update the existing resources in the target Kubernetes cluster. If no resources exist, it will fail. + */ + Replace, + /** + * To perform patch updates to the existing resources. If no resources exist, it will fail. + * More information in server-side-apply. + */ + ServerSideApply; +} diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItem.java index 396ef1a7d95a9..d4639300cc9d8 100644 --- a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItem.java +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItem.java @@ -35,23 +35,27 @@ public final class KubernetesDeploymentTargetBuildItem extends MultiBuildItem private final String version; private final int priority; private final boolean enabled; + private final DeployStrategy deployStrategy; - public KubernetesDeploymentTargetBuildItem(String name, String kind, String group, String version) { - this(name, kind, group, version, DEFAULT_PRIORITY, false); + public KubernetesDeploymentTargetBuildItem(String name, String kind, String group, String version, + DeployStrategy deployStrategy) { + this(name, kind, group, version, DEFAULT_PRIORITY, false, deployStrategy); } - public KubernetesDeploymentTargetBuildItem(String name, String kind, String group, String version, boolean enabled) { - this(name, kind, group, version, DEFAULT_PRIORITY, enabled); + public KubernetesDeploymentTargetBuildItem(String name, String kind, String group, String version, boolean enabled, + DeployStrategy deployStrategy) { + this(name, kind, group, version, DEFAULT_PRIORITY, enabled, deployStrategy); } public KubernetesDeploymentTargetBuildItem(String name, String kind, String group, String version, int priority, - boolean enabled) { + boolean enabled, DeployStrategy deployStrategy) { this.name = Objects.requireNonNull(name, "'name' must not be null"); this.kind = Objects.requireNonNull(kind, "'kind' must not be null"); this.version = Objects.requireNonNull(version, "'version' must not be null"); this.group = group; this.priority = priority; this.enabled = enabled; + this.deployStrategy = deployStrategy; } public String getGroup() { @@ -78,6 +82,10 @@ public boolean isEnabled() { return enabled; } + public DeployStrategy getDeployStrategy() { + return deployStrategy; + } + public boolean nameAndKindMatch(KubernetesDeploymentTargetBuildItem other) { return this.name.equals(other.getName()) && this.kind.equals(other.getKind()); } @@ -103,7 +111,8 @@ private KubernetesDeploymentTargetBuildItem merge(KubernetesDeploymentTargetBuil return new KubernetesDeploymentTargetBuildItem(this.name, this.kind, this.group, this.version, Math.max(this.priority, other.getPriority()), - this.enabled || other.isEnabled()); + this.enabled || other.isEnabled(), + this.deployStrategy); } @Override diff --git a/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItemTest.java b/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItemTest.java index 76ab5e84e5117..bcadb87906cce 100644 --- a/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItemTest.java +++ b/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesDeploymentTargetBuildItemTest.java @@ -1,5 +1,6 @@ package io.quarkus.kubernetes.spi; +import static io.quarkus.kubernetes.spi.DeployStrategy.CreateOrUpdate; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; @@ -12,21 +13,21 @@ class KubernetesDeploymentTargetBuildItemTest { @Test void testMergeList() { List input = Arrays.asList( - new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", 0, false), - new KubernetesDeploymentTargetBuildItem("n2", "k2", "g1", "v1", 10, false), - new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", -10, false), - new KubernetesDeploymentTargetBuildItem("n3", "k3", "g1", "v1", Integer.MIN_VALUE, false), - new KubernetesDeploymentTargetBuildItem("n4", "k4", "g1", "v1", Integer.MIN_VALUE, true), - new KubernetesDeploymentTargetBuildItem("n2", "k2", "g1", "v1", -10, true), - new KubernetesDeploymentTargetBuildItem("n4", "k4", "g1", "v1", Integer.MAX_VALUE, true), - new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", 100, false)); + new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", 0, false, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n2", "k2", "g1", "v1", 10, false, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", -10, false, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n3", "k3", "g1", "v1", Integer.MIN_VALUE, false, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n4", "k4", "g1", "v1", Integer.MIN_VALUE, true, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n2", "k2", "g1", "v1", -10, true, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n4", "k4", "g1", "v1", Integer.MAX_VALUE, true, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", 100, false, CreateOrUpdate)); List result = KubernetesDeploymentTargetBuildItem.mergeList(input); assertThat(result).containsOnly( - new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", 100, false), - new KubernetesDeploymentTargetBuildItem("n2", "k2", "g1", "v1", 10, true), - new KubernetesDeploymentTargetBuildItem("n3", "k3", "g1", "v1", Integer.MIN_VALUE, false), - new KubernetesDeploymentTargetBuildItem("n4", "k4", "g1", "v1", Integer.MAX_VALUE, true)); + new KubernetesDeploymentTargetBuildItem("n1", "k1", "g1", "v1", 100, false, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n2", "k2", "g1", "v1", 10, true, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n3", "k3", "g1", "v1", Integer.MIN_VALUE, false, CreateOrUpdate), + new KubernetesDeploymentTargetBuildItem("n4", "k4", "g1", "v1", Integer.MAX_VALUE, true, CreateOrUpdate)); } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DeploymentTargetEntry.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DeploymentTargetEntry.java index 8872e757fbf48..84ff998361e26 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DeploymentTargetEntry.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DeploymentTargetEntry.java @@ -1,14 +1,18 @@ package io.quarkus.kubernetes.deployment; +import io.quarkus.kubernetes.spi.DeployStrategy; + public class DeploymentTargetEntry { private final String name; private final String kind; private final int priority; + private final DeployStrategy deployStrategy; - public DeploymentTargetEntry(String name, String kind, int priority) { + public DeploymentTargetEntry(String name, String kind, int priority, DeployStrategy deployStrategy) { this.name = name; this.kind = kind; this.priority = priority; + this.deployStrategy = deployStrategy; } public String getName() { @@ -22,4 +26,8 @@ public String getKind() { public int getPriority() { return priority; } + + public DeployStrategy getDeployStrategy() { + return deployStrategy; + } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java index 639bf7d53c8a4..a1980bde84f5f 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java @@ -7,6 +7,7 @@ import io.dekorate.kubernetes.annotation.ImagePullPolicy; import io.dekorate.kubernetes.annotation.ServiceType; +import io.quarkus.kubernetes.spi.DeployStrategy; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -498,6 +499,12 @@ public EnvVarsConfig getEnv() { @ConfigItem(defaultValue = "false") boolean deploy; + /** + * If deploy is enabled, it will follow this strategy to update the resources to the target Knative cluster. + */ + @ConfigItem(defaultValue = "CreateOrUpdate") + DeployStrategy deployStrategy; + public Optional getAppSecret() { return this.appSecret; } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java index 1e224bf41a5d6..02e174e2257e3 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java @@ -90,8 +90,7 @@ public void checkKnative(ApplicationInfoBuildItem applicationInfo, KnativeConfig boolean knativeEnabled = targets.contains(KNATIVE); deploymentTargets.produce( new KubernetesDeploymentTargetBuildItem(KNATIVE, KNATIVE_SERVICE, KNATIVE_SERVICE_GROUP, - KNATIVE_SERVICE_VERSION, KNATIVE_PRIORITY, - knativeEnabled)); + KNATIVE_SERVICE_VERSION, KNATIVE_PRIORITY, knativeEnabled, config.deployStrategy)); if (knativeEnabled) { String name = ResourceNameUtil.getResourceName(config, applicationInfo); resourceMeta.produce(new KubernetesResourceMetadataBuildItem(KNATIVE, KNATIVE_SERVICE_GROUP, diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java index c07a97d49864b..30a85d31db6b7 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java @@ -15,6 +15,7 @@ import io.dekorate.kubernetes.annotation.ServiceType; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; +import io.quarkus.kubernetes.spi.DeployStrategy; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -303,6 +304,12 @@ public enum DeploymentResourceKind { @ConfigItem(defaultValue = "false") boolean deploy; + /** + * If deploy is enabled, it will follow this strategy to update the resources to the target Kubernetes cluster. + */ + @ConfigItem(defaultValue = "CreateOrUpdate") + DeployStrategy deployStrategy; + /** * If set, the secret will mounted to the application container and its contents will be used for application configuration. */ @@ -564,6 +571,10 @@ public boolean isIdempotent() { return idempotent; } + public DeployStrategy getDeployStrategy() { + return deployStrategy; + } + public KubernetesConfig.DeploymentResourceKind getDeploymentResourceKind(Capabilities capabilities) { if (deploymentKind.isPresent()) { return deploymentKind.get(); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java index d98164e55513c..38f874cafa6e0 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java @@ -33,6 +33,9 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; import io.fabric8.kubernetes.client.utils.ApiVersionUtil; import io.fabric8.openshift.api.model.Route; @@ -50,6 +53,7 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.kubernetes.client.deployment.KubernetesClientErrorHandler; import io.quarkus.kubernetes.client.spi.KubernetesClientBuildItem; +import io.quarkus.kubernetes.spi.DeployStrategy; import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem; import io.quarkus.kubernetes.spi.KubernetesDeploymentClusterBuildItem; import io.quarkus.kubernetes.spi.KubernetesOptionalResourceDefinitionBuildItem; @@ -212,42 +216,7 @@ private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, try (FileInputStream fis = new FileInputStream(manifest)) { KubernetesList list = Serialization.unmarshalAsList(fis); list.getItems().stream().filter(distinctByResourceKey()).forEach(i -> { - if (i instanceof GenericKubernetesResource) { - GenericKubernetesResource genericResource = (GenericKubernetesResource) i; - ResourceDefinitionContext context = getGenericResourceContext(client, genericResource) - .orElseThrow(() -> new IllegalStateException("Could not retrieve API resource information for:" - + i.getApiVersion() + " " + i.getKind() + ". Is the CRD for the resource available?")); - - client.genericKubernetesResources(context).resource(genericResource).createOrReplace(); - } else { - final var r = client.resource(i); - if (shouldDeleteExisting(deploymentTarget, i)) { - r.delete(); - try { - r.waitUntilCondition(Objects::isNull, 10, TimeUnit.SECONDS); - } catch (Exception e) { - if (e instanceof InterruptedException) { - throw e; - } - //This is something that should not really happen. it's not a fatal condition so let's just log. - log.warn("Failed to wait for the deletion of: " + i.getApiVersion() + " " + i.getKind() + " " - + i.getMetadata().getName() + ". Is the resource waitable?"); - } - } - try { - r.createOrReplace(); - } catch (Exception e) { - if (e instanceof InterruptedException) { - throw e; - } else if (isOptional(optionalResourceDefinitions, i)) { - log.warn("Failed to apply: " + i.getKind() + " " + i.getMetadata().getName() - + ", possibly due to missing a CRD apiVersion: " + i.getApiVersion() + " and kind: " - + i.getKind() + "."); - } else { - throw e; - } - } - } + deployResource(deploymentTarget, client, i, optionalResourceDefinitions); log.info("Applied: " + i.getKind() + " " + i.getMetadata().getName() + "."); }); @@ -268,6 +237,69 @@ private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, } + private void deployResource(DeploymentTargetEntry deploymentTarget, KubernetesClient client, HasMetadata metadata, + List optionalResourceDefinitions) { + var r = findResource(client, metadata); + if (shouldDeleteExisting(deploymentTarget, metadata)) { + deleteResource(metadata, r); + } + + try { + switch (deploymentTarget.getDeployStrategy()) { + case Create: + r.create(); + break; + case Replace: + r.replace(); + break; + case ServerSideApply: + r.patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY)); + break; + default: + r.createOrReplace(); + break; + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + throw e; + } else if (isOptional(optionalResourceDefinitions, metadata)) { + log.warn("Failed to apply: " + metadata.getKind() + " " + metadata.getMetadata().getName() + + ", possibly due to missing a CRD apiVersion: " + metadata.getApiVersion() + " and kind: " + + metadata.getKind() + "."); + } else { + throw e; + } + } + } + + private void deleteResource(HasMetadata metadata, Resource r) { + r.delete(); + try { + r.waitUntilCondition(Objects::isNull, 10, TimeUnit.SECONDS); + } catch (Exception e) { + if (e instanceof InterruptedException) { + throw e; + } + //This is something that should not really happen. it's not a fatal condition so let's just log. + log.warn("Failed to wait for the deletion of: " + metadata.getApiVersion() + " " + metadata.getKind() + " " + + metadata.getMetadata().getName() + ". Is the resource waitable?"); + } + } + + private Resource findResource(KubernetesClient client, HasMetadata metadata) { + if (metadata instanceof GenericKubernetesResource) { + GenericKubernetesResource genericResource = (GenericKubernetesResource) metadata; + ResourceDefinitionContext context = getGenericResourceContext(client, genericResource) + .orElseThrow(() -> new IllegalStateException("Could not retrieve API resource information for:" + + metadata.getApiVersion() + " " + metadata.getKind() + + ". Is the CRD for the resource available?")); + + return client.genericKubernetesResources(context).resource(genericResource); + } + + return client.resource(metadata); + } + private void printExposeInformation(KubernetesClient client, KubernetesList list, OpenshiftConfig openshiftConfig, ApplicationInfoBuildItem applicationInfo) { String generatedRouteName = ResourceNameUtil.getResourceName(openshiftConfig, applicationInfo); @@ -322,6 +354,10 @@ private static boolean isOptional(List c.contains(OPENSHIFT) || c.contains(S2I)).isPresent(); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index c8effd4e10a99..894acfe573299 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -83,7 +83,7 @@ public void checkOpenshift(ApplicationInfoBuildItem applicationInfo, Capabilitie DeploymentResourceKind deploymentResourceKind = config.getDeploymentResourceKind(capabilities); deploymentTargets.produce( new KubernetesDeploymentTargetBuildItem(OPENSHIFT, deploymentResourceKind.kind, deploymentResourceKind.apiGroup, - deploymentResourceKind.apiVersion, OPENSHIFT_PRIORITY, openshiftEnabled)); + deploymentResourceKind.apiVersion, OPENSHIFT_PRIORITY, openshiftEnabled, config.deployStrategy)); if (openshiftEnabled) { String name = ResourceNameUtil.getResourceName(config, applicationInfo); resourceMeta.produce(new KubernetesResourceMetadataBuildItem(OPENSHIFT, deploymentResourceKind.apiGroup, diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index 3fb365bb73888..6a6aa9cf56dda 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -73,8 +73,7 @@ public void checkVanillaKubernetes(ApplicationInfoBuildItem applicationInfo, Cap deploymentTargets .produce( new KubernetesDeploymentTargetBuildItem(KUBERNETES, kind, DEPLOYMENT_GROUP, - DEPLOYMENT_VERSION, - VANILLA_KUBERNETES_PRIORITY, true)); + DEPLOYMENT_VERSION, VANILLA_KUBERNETES_PRIORITY, true, config.deployStrategy)); String name = ResourceNameUtil.getResourceName(config, applicationInfo); resourceMeta.produce(new KubernetesResourceMetadataBuildItem(KUBERNETES, DEPLOYMENT_GROUP, DEPLOYMENT_VERSION, @@ -83,8 +82,7 @@ public void checkVanillaKubernetes(ApplicationInfoBuildItem applicationInfo, Cap } else { deploymentTargets .produce(new KubernetesDeploymentTargetBuildItem(KUBERNETES, kind, DEPLOYMENT_GROUP, - DEPLOYMENT_VERSION, - VANILLA_KUBERNETES_PRIORITY, false)); + DEPLOYMENT_VERSION, VANILLA_KUBERNETES_PRIORITY, false, config.deployStrategy)); } }