Skip to content

Commit

Permalink
Merge pull request #35036 from iocanel/push-pull-secret-handling
Browse files Browse the repository at this point in the history
Generate/use pull secrets when possible
  • Loading branch information
iocanel authored Aug 8, 2023
2 parents bc75d66 + 60b2237 commit 6f55d65
Show file tree
Hide file tree
Showing 16 changed files with 209 additions and 35 deletions.
35 changes: 35 additions & 0 deletions docs/src/main/asciidoc/deploying-to-kubernetes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,41 @@ quarkus.container-image.registry=my.docker-registry.net
By adding this property along with the rest of the container image properties of the previous section, the generated manifests will use the image `my.docker-registry.net/quarkus/demo-app:1.0`.
The image is not the only thing that can be customized in the generated manifests, as will become evident in the following sections.


==== Automatic generation of pull secrets

When docker registries are used, users often provide credentials, so that an image is built and pushed to the specified registry during the build.
[source,properties]
----
quarkus.container-image.username=myusername
quarkus.container-image.password=mypassword
----

Kubernetes will also need these credentials when it comes to pull the image from the registry. This is where image pull secrets are used. An image pull secret is a special kind
of secret that contains the required credentials. Quarkus can automatically generate and configure when:

[source,properties]
----
quarkus.kubernetes.generate-image-pull-secret=true
----

More specifically a `Secret`like the one bellow is genrated:

[source,yaml]
----
apiVersion: v1
kind: Secret
metadata:
name: test-quarkus-app-pull-secret
data:
".dockerconfigjson": ewogCSJhdXRocyI6IHsKCQkibXkucmVnaXN0eS5vcmciOiB7CiAJCQkiYXV0aCI6ImJYbDFjMlZ5Ym1GdFpUcHRlWEJoYzNOM2IzSmsiCgkJfQoJfQp9
type: kubernetes.io/dockerconfigjson
----

And also `test-quarkus-app-pull-secret is added to the `imagePullSecrets` list.


=== Labels and Annotations

==== Labels
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.jboss.logging.Logger;

import io.dekorate.kubernetes.decorator.AddDockerConfigJsonSecretDecorator;
import io.dekorate.kubernetes.decorator.AddImagePullSecretDecorator;
import io.dekorate.utils.Packaging;
import io.dekorate.utils.Serialization;
import io.fabric8.kubernetes.api.model.HasMetadata;
Expand Down Expand Up @@ -262,7 +261,6 @@ public void configureExternalRegistry(ApplicationInfoBuildItem applicationInfo,
applicationInfo.getName(), containerImageInfo.getImage(), imagePushSecret)));
decorator.produce(new DecoratorBuildItem(OPENSHIFT,
new ApplyDockerImageRepositoryToImageStream(applicationInfo.getName(), repositoryWithRegistry)));
decorator.produce(new DecoratorBuildItem(OPENSHIFT, new AddImagePullSecretDecorator(name, imagePushSecret)));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static List<DecoratorBuildItem> createDecorators(String clusterKind,
Optional<Port> port = KubernetesCommonHelper.getPort(ports, config);
result.addAll(KubernetesCommonHelper.createDecorators(project, clusterKind, name, config,
metricsConfiguration, kubernetesClientConfiguration,
annotations, labels, command,
annotations, labels, image, command,
port, livenessPath, readinessPath, startupPath, roles, clusterRoles, serviceAccounts, roleBindings));

image.ifPresent(i -> {
Expand Down Expand Up @@ -181,4 +181,4 @@ private static int getStablePortNumberInRange(String input, int min, int max) {
throw new RuntimeException("Unable to generate stable port number from input string: '" + input + "'", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ public class KnativeConfig implements PlatformConfiguration {
@ConfigItem
Optional<List<String>> imagePullSecrets;

/**
* Enable generation of image pull secret, when the container image username and
* password are provided.
*/
@ConfigItem(defaultValue = "false")
boolean generateImagePullSecret;

/**
* The liveness probe
*/
Expand Down Expand Up @@ -329,6 +336,10 @@ public Optional<List<String>> getImagePullSecrets() {
return imagePullSecrets;
}

public boolean isGenerateImagePullSecret() {
return generateImagePullSecret;
}

public ProbeConfig getLivenessProbe() {
return livenessProbe;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public List<DecoratorBuildItem> createDecorators(ApplicationInfoBuildItem applic
Optional<Port> port = KubernetesCommonHelper.getPort(ports, config, "http");
result.addAll(KubernetesCommonHelper.createDecorators(project, KNATIVE, name, config,
metricsConfiguration, kubernetesClientConfiguration, annotations,
labels, command, port, livenessPath, readinessPath, startupProbePath,
labels, image, command, port, livenessPath, readinessPath, startupProbePath,
roles, clusterRoles, serviceAccounts, roleBindings));

image.ifPresent(i -> {
Expand Down Expand Up @@ -375,4 +375,4 @@ private static List<DecoratorBuildItem> createVolumeDecorators(Optional<Project>
});
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import io.dekorate.kubernetes.decorator.AddAzureDiskVolumeDecorator;
import io.dekorate.kubernetes.decorator.AddAzureFileVolumeDecorator;
import io.dekorate.kubernetes.decorator.AddConfigMapVolumeDecorator;
import io.dekorate.kubernetes.decorator.AddDockerConfigJsonSecretDecorator;
import io.dekorate.kubernetes.decorator.AddEmptyDirVolumeDecorator;
import io.dekorate.kubernetes.decorator.AddEnvVarDecorator;
import io.dekorate.kubernetes.decorator.AddHostAliasesDecorator;
Expand Down Expand Up @@ -78,6 +79,7 @@
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.rbac.PolicyRule;
import io.fabric8.kubernetes.api.model.rbac.PolicyRuleBuilder;
import io.quarkus.container.spi.ContainerImageInfoBuildItem;
import io.quarkus.deployment.builditem.ApplicationInfoBuildItem;
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
import io.quarkus.deployment.pkg.PackageConfig;
Expand Down Expand Up @@ -228,6 +230,7 @@ public static List<DecoratorBuildItem> createDecorators(Optional<Project> projec
Optional<KubernetesClientCapabilityBuildItem> kubernetesClientConfiguration,
List<KubernetesAnnotationBuildItem> annotations,
List<KubernetesLabelBuildItem> labels,
Optional<ContainerImageInfoBuildItem> image,
Optional<KubernetesCommandBuildItem> command,
Optional<Port> port,
Optional<KubernetesHealthLivenessPathBuildItem> livenessProbePath,
Expand All @@ -249,6 +252,20 @@ public static List<DecoratorBuildItem> createDecorators(Optional<Project> projec
result.addAll(createCommandDecorator(project, target, name, config, command));
result.addAll(createArgsDecorator(project, target, name, config, command));

// Handle Pull Secrets
if (config.isGenerateImagePullSecret()) {
image.ifPresent(i -> {
i.getRegistry().ifPresent(registry -> {
if (i.getUsername().isPresent() && i.getPassword().isPresent()) {
String imagePullSecret = name + "-pull-secret";
result.add(new DecoratorBuildItem(target, new AddImagePullSecretDecorator(name, imagePullSecret)));
result.add(new DecoratorBuildItem(target, new AddDockerConfigJsonSecretDecorator(imagePullSecret,
registry, i.username.get(), i.password.get())));
}
});
});
}

// Handle Probes
if (!port.isEmpty()) {
result.addAll(createProbeDecorators(name, target, config.getLivenessProbe(), config.getReadinessProbe(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ public enum DeploymentResourceKind {
@ConfigItem
Optional<List<String>> imagePullSecrets;

/**
* Enable generation of image pull secret, when the container image username and
* password are provided.
*/
@ConfigItem(defaultValue = "false")
boolean generateImagePullSecret;

/**
* The liveness probe
*/
Expand Down Expand Up @@ -510,6 +517,10 @@ public Optional<List<String>> getImagePullSecrets() {
return imagePullSecrets;
}

public boolean isGenerateImagePullSecret() {
return generateImagePullSecret;
}

public ProbeConfig getLivenessProbe() {
return livenessProbe;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ public static enum DeploymentResourceKind {
@ConfigItem
Optional<List<String>> imagePullSecrets;

/**
* Enable generation of image pull secret, when the container image username and
* password are provided.
*/
@ConfigItem(defaultValue = "false")
boolean generateImagePullSecret;

/**
* The liveness probe
*/
Expand Down Expand Up @@ -412,6 +419,10 @@ public Optional<List<String>> getImagePullSecrets() {
return imagePullSecrets;
}

public boolean isGenerateImagePullSecret() {
return generateImagePullSecret;
}

public ProbeConfig getLivenessProbe() {
return livenessProbe;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public List<DecoratorBuildItem> createDecorators(ApplicationInfoBuildItem applic
Optional<Port> port = KubernetesCommonHelper.getPort(ports, config, config.route.targetPort);
result.addAll(KubernetesCommonHelper.createDecorators(project, OPENSHIFT, name, config,
metricsConfiguration, kubernetesClientConfiguration,
annotations, labels, command,
annotations, labels, image, command,
port, livenessPath, readinessPath, startupPath, roles, clusterRoles, serviceAccounts, roleBindings));

if (config.flavor == v3) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public interface PlatformConfiguration extends EnvVarHolder {

Optional<List<String>> getImagePullSecrets();

boolean isGenerateImagePullSecret();

ProbeConfig getLivenessProbe();

ProbeConfig getReadinessProbe();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public List<DecoratorBuildItem> createDecorators(ApplicationInfoBuildItem applic
packageConfig);
Optional<Port> port = KubernetesCommonHelper.getPort(ports, config);
result.addAll(KubernetesCommonHelper.createDecorators(project, KUBERNETES, name, config,
metricsConfiguration, kubernetesClientConfiguration, annotations, labels, command, port,
metricsConfiguration, kubernetesClientConfiguration, annotations, labels, image, command, port,
livenessPath, readinessPath, startupPath,
roles, clusterRoles, serviceAccounts, roleBindings));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,29 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.openshift.api.model.BuildConfig;
import io.fabric8.openshift.api.model.DeploymentConfig;
import io.fabric8.openshift.api.model.ImageStream;

public class BaseOpenshiftWithRemoteRegistry {
public class BaseOpenshiftWithRemoteRegistry extends BaseWithRemoteRegistry {

public void assertGeneratedResources(String name, String tag, Path buildDir) throws IOException {
Path kubernetesDir = buildDir.resolve("kubernetes");
List<HasMetadata> resourceList = getResources("openshift", buildDir);
assertGeneratedResources(name, tag, resourceList);
}

assertThat(kubernetesDir).isDirectoryContaining(p -> p.getFileName().endsWith("openshift.json"))
.isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml"));
List<HasMetadata> openshiftList = DeserializationUtil.deserializeAsList(kubernetesDir.resolve("openshift.yml"));
public void assertGeneratedResources(String name, String tag, List<HasMetadata> resourceList) throws IOException {
super.assertGeneratedResources(name, resourceList);

assertThat(openshiftList).filteredOn(h -> "DeploymentConfig".equals(h.getKind())).singleElement().satisfies(h -> {
assertThat(h.getMetadata()).satisfies(m -> {
assertThat(m.getName()).isEqualTo(name);
});
assertThat(h).isInstanceOfSatisfying(DeploymentConfig.class, d -> {
assertThat(d.getSpec().getTemplate().getSpec().getImagePullSecrets()).singleElement().satisfies(l -> {
assertThat(l.getName()).isEqualTo(name + "-push-secret");
assertThat(resourceList)
.filteredOn(
h -> "Secret".equals(h.getKind()) && h.getMetadata().getName().equals(name + "-push-secret"))
.singleElement().satisfies(h -> {
assertThat(h).isInstanceOfSatisfying(Secret.class, s -> {
assertThat(s.getType()).isEqualTo("kubernetes.io/dockerconfigjson");
assertThat(s.getData()).containsKey(".dockerconfigjson");
});
});
});
});

assertThat(openshiftList).filteredOn(h -> "Secret".equals(h.getKind())).singleElement().satisfies(h -> {
assertThat(h.getMetadata()).satisfies(m -> {
assertThat(m.getName()).isEqualTo(name + "-push-secret");
});
assertThat(h).isInstanceOfSatisfying(Secret.class, s -> {
assertThat(s.getType()).isEqualTo("kubernetes.io/dockerconfigjson");
assertThat(s.getData()).containsKey(".dockerconfigjson");
});
});

assertThat(openshiftList).filteredOn(h -> "BuildConfig".equals(h.getKind())).singleElement().satisfies(h -> {
assertThat(resourceList).filteredOn(h -> "BuildConfig".equals(h.getKind())).singleElement().satisfies(h -> {
assertThat(h.getMetadata()).satisfies(m -> {
assertThat(m.getName()).isEqualTo(name);
});
Expand All @@ -52,7 +41,7 @@ public void assertGeneratedResources(String name, String tag, Path buildDir) thr
});
});

assertThat(openshiftList)
assertThat(resourceList)
.filteredOn(h -> "ImageStream".equals(h.getKind()) && h.getMetadata().getName().equals(name))
.singleElement().satisfies(h -> {
assertThat(h).isInstanceOfSatisfying(ImageStream.class, i -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.quarkus.it.kubernetes;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.LocalObjectReference;
import io.fabric8.kubernetes.api.model.PodSpecFluent;
import io.fabric8.kubernetes.api.model.Secret;

public class BaseWithRemoteRegistry {

public void assertGeneratedResources(String name, String target, Path buildDir) throws IOException {
List<HasMetadata> resourceList = getResources(target, buildDir);
assertGeneratedResources(name, resourceList);
}

List<HasMetadata> getResources(String target, Path buildDir) throws IOException {
Path kubernetesDir = buildDir.resolve("kubernetes");

assertThat(kubernetesDir).isDirectoryContaining(p -> p.getFileName().endsWith(target + ".json"))
.isDirectoryContaining(p -> p.getFileName().endsWith(target + ".yml"));
return DeserializationUtil.deserializeAsList(kubernetesDir.resolve(target + ".yml"));
}

public void assertGeneratedResources(String name, List<HasMetadata> resourceList) {
assertThat(resourceList).satisfies(r -> {
KubernetesList kubernetesList = new KubernetesListBuilder()
.addAllToItems(resourceList)
.accept(PodSpecFluent.class, spec -> {
assertThat(spec.buildImagePullSecrets()).singleElement().satisfies(e -> {
assertThat(e).isInstanceOfSatisfying(LocalObjectReference.class, l -> {
assertThat(l.getName()).isEqualTo(name + "-pull-secret");
});
});

}).build();
});

assertThat(resourceList)
.filteredOn(
h -> "Secret".equals(h.getKind()) && h.getMetadata().getName().equals(name + "-pull-secret"))
.singleElement().satisfies(h -> {
assertThat(h).isInstanceOfSatisfying(Secret.class, s -> {
assertThat(s.getType()).isEqualTo("kubernetes.io/dockerconfigjson");
assertThat(s.getData()).containsKey(".dockerconfigjson");
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.quarkus.it.kubernetes;

import java.io.IOException;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.ProdBuildResults;
import io.quarkus.test.ProdModeTestResults;
import io.quarkus.test.QuarkusProdModeTest;

public class KubernetesWithImagePushTest extends BaseWithRemoteRegistry {

private static final String APP_NAME = "kubernetes-with-remote-image-push";

@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class))
.setApplicationName(APP_NAME)
.setApplicationVersion("0.1-SNAPSHOT")
.overrideConfigKey("quarkus.container-image.group", "user")
.overrideConfigKey("quarkus.container-image.image", "quay.io/user/" + APP_NAME + ":1.0")
.overrideConfigKey("quarkus.container-image.username", "me")
.overrideConfigKey("quarkus.container-image.password", "pass")
.overrideConfigKey("quarkus.kubernetes.generate-image-pull-secret", "true")
.setForcedDependencies(List.of(
Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-container-image-docker", Version.getVersion())));

@ProdBuildResults
private ProdModeTestResults prodModeTestResults;

@Test
public void assertGeneratedResources() throws IOException {
assertGeneratedResources(APP_NAME, "kubernetes", prodModeTestResults.getBuildDir());
}
}
Loading

0 comments on commit 6f55d65

Please sign in to comment.