Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/src/main/asciidoc/kubernetes-dev-services.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ And then add the following property to your `application.properties`:
quarkus.kubernetes-client.devservices.manifests=kubernetes/namespace.yaml
```

When applying multiple properties, you can apply them as:
```
quarkus.kubernetes-client.devservices.manifests=kubernetes/first_manifest.yaml,kubernetes/second_manifest.yaml
```

It is also possible to apply manifests from a URL. So the following syntax would also be possible:
```
quarkus.kubernetes-client.devservices.manifests=https://example.com/kubernetes/namespace.yaml
```

NOTE: Quarkus will apply the manifests in the order they are specified in the list.

NOTE: Quarkus will await resources such as deployments to be ready before moving on to the next manifest.
All applied resources are guaranteed to be ready by the time your application finishes startup.

=== Deploying helm charts
`quarkus.kubernetes-client.devservices.manifests` only supports manifests. If you want to deploy a helm chart, you can use the `k3s` flavor and deploy a `HelmChart` manifest. See https://docs.k3s.io/helm for more information on how to use helm charts with k3s.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -45,8 +44,12 @@
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.InspectContainerResponse;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.ReplicaSet;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.client.*;
import io.fabric8.kubernetes.client.Config;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.IsDevServicesSupportedByLaunchMode;
import io.quarkus.deployment.annotations.BuildProducer;
Expand Down Expand Up @@ -196,10 +199,7 @@ public void applyManifests(
.withConfig(Config.fromKubeconfig(kubernetesDevServiceInfoBuildItem.getKubeConfig()))
.build()) {
for (String manifestPath : manifests.get()) {
// Load the manifest from the resources directory
InputStream manifestStream = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(manifestPath);
InputStream manifestStream = getManifestStream(manifestPath);

if (manifestStream == null) {
log.errorf("Could not find manifest file in resources: %s", manifestPath);
Expand All @@ -210,21 +210,63 @@ public void applyManifests(
try {
// A single manifest file may contain multiple resources to deploy
List<HasMetadata> resources = client.load(manifestStream).items();
List<HasMetadata> resourcesWithReadiness = new ArrayList<>();

if (resources.isEmpty()) {
log.warnf("No resources found in manifest: %s", manifestPath);
} else {
resources.forEach(resource -> client.resource(resource).create());
resources.forEach(resource -> {
client.resource(resource).create();

if (isReadinessApplicable(resource)) {
resourcesWithReadiness.add(resource);
}
});

resourcesWithReadiness.forEach(resource -> {
log.info("Waiting for " + resource.getClass().getSimpleName() + " "
+ resource.getMetadata().getName()
+ " to be ready...");
client.resource(resource).waitUntilReady(60, TimeUnit.SECONDS);
});
}
} catch (Exception ex) {
log.errorf("Failed to deploy manifest %s: %s", manifestPath, ex.getMessage());
log.errorf("Failed to apply manifest %s: %s", manifestPath, ex.getMessage());
}
}

log.infof("Applied manifest %s.", manifestPath);
}
} catch (Exception e) {
log.error("Failed to create Kubernetes client while trying to deploy manifests.", e);
log.error("Failed to create Kubernetes client while trying to apply manifests.", e);
}
}

private boolean isReadinessApplicable(HasMetadata item) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created a PR: fabric8io/kubernetes-client#7206, we should use the implementation from the client's Readiness (accessible from the Config) instead, if / when the PR is merged and available.

Copy link
Contributor Author

@LarsSven LarsSven Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah perfect! That is indeed exactly why I copied the function over, because it wasn't available publicly

Copy link
Contributor Author

@LarsSven LarsSven Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably has to go through the whole cycle though of merge -> release -> upgrade in Quarkus, then we can change this?

Would it be possible to merge this PR with the copied over function for now and I'll make sure I'll make a followup PR once the function is available? So we can make the next LTS Quarkus release with these changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new version of fabric8 has been released and merged in, however I'm still not happy with the public API as it's non-static, I made a followup PR before we remove the copied method: fabric8io/kubernetes-client#7280

return (item instanceof Deployment ||
item instanceof io.fabric8.kubernetes.api.model.extensions.Deployment ||
item instanceof ReplicaSet ||
item instanceof Pod ||
item instanceof ReplicationController ||
item instanceof Endpoints ||
item instanceof Node ||
item instanceof StatefulSet);
}

private InputStream getManifestStream(String manifestPath) throws IOException {
try {
URL url = new URL(manifestPath);
// For file:// URLs, optionally check if you want to support those or not
return url.openStream();
} catch (MalformedURLException e) {
// Not a URL, so treat as classpath resource
InputStream stream = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(manifestPath);
if (stream == null) {
throw new IOException("Resource not found: " + manifestPath);
}
return stream;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
quarkus.kubernetes-client.devservices.manifests=kubernetes/namespace.yaml
quarkus.kubernetes-client.devservices.manifests=kubernetes/namespace.yaml,https://k8s.io/examples/admin/namespace-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ public void shouldReturnAllKeys() {
}

@Test
@DisplayName("specified manifest must be applied to the cluster by the dev service")
public void manifestIsApplied() {
@DisplayName("specified manifest in the resources folder must be applied to the cluster by the dev service")
public void resourceManifestIsApplied() {
Assertions.assertNotNull(kubernetesClient.namespaces().withName("example-namespace").get());
}

@Test
@DisplayName("specified manifest from a URL must be applied to the cluster by the dev service")
public void urlManifestIsApplied() {
// Applied by https://k8s.io/examples/admin/namespace-dev.yaml
Assertions.assertNotNull(kubernetesClient.namespaces().withName("development").get());
}
}
Loading