Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate/use pull secrets when possible #35036

Merged
merged 1 commit into from
Aug 8, 2023
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
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 @@ -170,6 +170,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 @@ -513,6 +520,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 @@ -222,7 +222,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