From 54098bbbfa29ec75ee71cb4ca928305b712f45ba Mon Sep 17 00:00:00 2001 From: Jose Date: Mon, 13 Mar 2023 09:46:26 +0100 Subject: [PATCH] Fully support generation of K8s RBAC resources These changes address a long-time issue in regards of K8s RBAC resources (see related issues). These changes allow to generate custom Roles, ClusterRoles, ServiceAccount, and RoleBindings. Plus, it allows the Kubernetes Client and Kubernetes Config extensions to configure the role binding to generate. Fix https://github.com/quarkusio/quarkus/issues/16612 Fix https://github.com/quarkusio/quarkus/issues/19286 Fix https://github.com/quarkusio/quarkus/issues/15422 --- .../asciidoc/deploying-to-kubernetes.adoc | 4 +- .../deployment/KubernetesClientProcessor.java | 19 +- .../runtime/KubernetesClientBuildConfig.java | 8 +- .../client/runtime/RoleBindingConfig.java | 65 ++++++ .../deployment/KubernetesConfigProcessor.java | 33 ++- .../KubernetesConfigBuildTimeConfig.java | 5 + .../config/runtime/SecretsRoleConfig.java | 34 ++++ .../kind/deployment/KindProcessor.java | 6 +- .../deployment/MinikubeProcessor.java | 6 +- .../spi/KubernetesClusterRoleBuildItem.java | 43 ++++ .../spi/KubernetesRoleBindingBuildItem.java | 96 +++++++-- .../spi/KubernetesRoleBuildItem.java | 57 ++---- .../KubernetesServiceAccountBuildItem.java | 60 ++++++ .../io/quarkus/kubernetes/spi/PolicyRule.java | 47 +++++ .../AddClusterRoleResourceDecorator.java | 48 +++++ .../AddNamespaceToSubjectDecorator.java | 4 +- .../AddRoleBindingResourceDecorator.java | 70 +++++++ .../deployment/AddRoleResourceDecorator.java | 50 +++-- .../AddServiceAccountResourceDecorator.java | 46 +++++ .../deployment/ClusterRoleConfig.java | 29 +++ .../kubernetes/deployment/Constants.java | 6 + .../deployment/DevClusterHelper.java | 6 +- .../deployment/InitTaskProcessor.java | 3 +- .../kubernetes/deployment/KnativeConfig.java | 11 + .../deployment/KnativeProcessor.java | 7 +- .../deployment/KubernetesCommonHelper.java | 188 +++++++++++++++--- .../deployment/KubernetesConfig.java | 10 + .../deployment/OpenshiftConfig.java | 10 + .../deployment/OpenshiftProcessor.java | 6 +- .../deployment/PlatformConfiguration.java | 2 + .../deployment/PolicyRuleConfig.java | 40 ++++ .../kubernetes/deployment/RbacConfig.java | 34 ++++ .../deployment/RoleBindingConfig.java | 44 ++++ .../kubernetes/deployment/RoleConfig.java | 35 ++++ .../deployment/ServiceAccountConfig.java | 35 ++++ .../kubernetes/deployment/SubjectConfig.java | 35 ++++ .../VanillaKubernetesProcessor.java | 7 +- ...esConfigWithSecretsAndClusterRoleTest.java | 94 +++++++++ .../KubernetesConfigWithSecretsTest.java | 14 +- .../KubernetesWithRbacAndNamespaceTest.java | 8 +- .../it/kubernetes/KubernetesWithRbacTest.java | 117 +++++++++++ ...ernetesClientAndCustomRbacBindingTest.java | 66 ++++++ .../kubernetes/WithKubernetesClientTest.java | 20 +- ...g-with-secrets-and-cluster-role.properties | 4 + ...-client-and-custom-rbac-binding.properties | 8 + .../resources/kubernetes-with-rbac.properties | 18 ++ 46 files changed, 1421 insertions(+), 137 deletions(-) create mode 100644 extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/RoleBindingConfig.java create mode 100644 extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/SecretsRoleConfig.java create mode 100644 extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesClusterRoleBuildItem.java create mode 100644 extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesServiceAccountBuildItem.java create mode 100644 extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/PolicyRule.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleBindingResourceDecorator.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PolicyRuleConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RbacConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SubjectConfig.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsAndClusterRoleTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndCustomRbacBindingTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets-and-cluster-role.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-and-custom-rbac-binding.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac.properties diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 9186346d280e81..6c63548b62d9a5 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -860,8 +860,8 @@ implementation("io.quarkus:quarkus-kubernetes-client") ---- To access the API server from within a Kubernetes cluster, some RBAC related resources are required (e.g. a ServiceAccount, a RoleBinding). -So, when the `kubernetes-client` extension is present, the `kubernetes` extension is going to create those resources automatically, so that application will be granted the `view` role. -If more roles are required, they will have to be added manually. +So, when the `kubernetes-client` extension is present, by default the `kubernetes` extension is going to create those resources automatically, so that application will be granted the `view` role. You can fully customize the roles and subjects to use using the properties under `quarkus.kubernetes-client.role-binding`. +If more roles are required, they can create them using the properties under `quarkus.kubernetes.rbac.roles`. [NOTE] ==== diff --git a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java index 6b30a409b2c197..3d13ef439c3a8c 100644 --- a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java +++ b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java @@ -54,7 +54,9 @@ import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig; import io.quarkus.kubernetes.client.runtime.KubernetesClientProducer; import io.quarkus.kubernetes.client.runtime.KubernetesConfigProducer; +import io.quarkus.kubernetes.client.runtime.RoleBindingConfig; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; import io.quarkus.maven.dependency.ArtifactKey; public class KubernetesClientProcessor { @@ -69,6 +71,7 @@ public class KubernetesClientProcessor { private static final DotName KUBE_SCHEMA = DotName.createSimple(KubeSchema.class.getName()); private static final DotName VISITABLE_BUILDER = DotName.createSimple(VisitableBuilder.class.getName()); private static final DotName CUSTOM_RESOURCE = DotName.createSimple(CustomResource.class.getName()); + private static final String SERVICE_ACCOUNT = "ServiceAccount"; private static final DotName JSON_FORMAT = DotName.createSimple(JsonFormat.class.getName()); private static final String[] EMPTY_STRINGS_ARRAY = new String[0]; @@ -106,12 +109,26 @@ public void process(ApplicationIndexBuildItem applicationIndex, CombinedIndexBui BuildProducer reflectiveClasses, BuildProducer reflectiveHierarchies, BuildProducer ignoredJsonDeserializationClasses, + BuildProducer serviceAccountProducer, BuildProducer roleBindingProducer, BuildProducer serviceProviderProducer) { featureProducer.produce(new FeatureBuildItem(Feature.KUBERNETES_CLIENT)); if (kubernetesClientConfig.generateRbac) { - roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view", true)); + RoleBindingConfig rbacConfig = kubernetesClientConfig.roleBinding; + if (SERVICE_ACCOUNT.equals(rbacConfig.subjectKind) && rbacConfig.subjectName.isEmpty()) { + // generate default service account resource + serviceAccountProducer.produce(new KubernetesServiceAccountBuildItem(true)); + } + + roleBindingProducer.produce( + new KubernetesRoleBindingBuildItem(rbacConfig.name.orElse(null), null, rbacConfig.labels, + new KubernetesRoleBindingBuildItem.RoleRef(rbacConfig.roleName, rbacConfig.clusterWide), + new KubernetesRoleBindingBuildItem.Subject( + rbacConfig.subjectApiGroup.orElse(null), + rbacConfig.subjectKind, + rbacConfig.subjectName.orElse(null), + rbacConfig.subjectNamespace.orElse(null)))); } // register fully (and not weakly) for reflection watchers, informers and custom resources diff --git a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java index bfdee6a2b635df..b07f97e5a00529 100644 --- a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java +++ b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java @@ -170,11 +170,17 @@ public class KubernetesClientBuildConfig { public Optional noProxy; /** - * Enable the generation of the RBAC manifests. + * Enable the generation of the RBAC manifests. If enabled, it will generate */ @ConfigItem(defaultValue = "true") public boolean generateRbac; + /** + * If generation of RBAC is enabled (see property `quarkus.kubernetes-client.generate-rbac`), then it will use the + * RBAC resources as specified under these properties. + */ + public RoleBindingConfig roleBinding; + /** * Dev Services */ diff --git a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/RoleBindingConfig.java b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/RoleBindingConfig.java new file mode 100644 index 00000000000000..00946408f3f4f7 --- /dev/null +++ b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/RoleBindingConfig.java @@ -0,0 +1,65 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class RoleBindingConfig { + + /** + * Name of the RoleBinding resource to be generated. If not provided, it will use the application name plus the role + * ref name. + */ + @ConfigItem + public Optional name; + + /** + * Labels to add into the RoleBinding resource. + */ + @ConfigItem + public Map labels; + + /** + * The "kind" resource to use by the Subject element in the generated Role Binding resource. + * By default, it's "ServiceAccount" kind. + */ + @ConfigItem(defaultValue = "ServiceAccount") + public String subjectKind; + + /** + * The "apiGroup" resource that matches with the "kind" property. By default, it's empty. + */ + @ConfigItem + public Optional subjectApiGroup; + + /** + * The "name" resource to use by the Subject element in the generated Role Binding resource. + * By default, it's the application name. + */ + @ConfigItem + public Optional subjectName; + + /** + * The "namespace" resource to use by the Subject element in the generated Role Binding resource. + * By default, it will use the same as provided in the generated resources. + */ + @ConfigItem + public Optional subjectNamespace; + + /** + * The name of the Role resource to use by the RoleRef element in the generated Role Binding resource. + * By default, it's "view" role name. + */ + @ConfigItem(defaultValue = "view") + public String roleName; + + /** + * If the Role sets in the `role-name` property is cluster wide or not. + * By default, it's "true". + */ + @ConfigItem(defaultValue = "true") + public boolean clusterWide; +} diff --git a/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java b/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java index 7a86348c5bb361..8c4aa31830066c 100644 --- a/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java +++ b/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java @@ -1,6 +1,5 @@ package io.quarkus.kubernetes.config.deployment; -import java.util.Collections; import java.util.List; import org.jboss.logmanager.Level; @@ -15,12 +14,21 @@ import io.quarkus.kubernetes.config.runtime.KubernetesConfigBuildTimeConfig; import io.quarkus.kubernetes.config.runtime.KubernetesConfigRecorder; import io.quarkus.kubernetes.config.runtime.KubernetesConfigSourceConfig; +import io.quarkus.kubernetes.config.runtime.SecretsRoleConfig; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.PolicyRule; import io.quarkus.runtime.TlsConfig; public class KubernetesConfigProcessor { + private static final String ANY_TARGET = null; + private static final List POLICY_RULE_FOR_ROLE = List.of(new PolicyRule( + List.of(""), + List.of("secrets"), + List.of("get"))); + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecorder recorder, @@ -36,15 +44,26 @@ public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecord public void handleAccessToSecrets(KubernetesConfigSourceConfig config, KubernetesConfigBuildTimeConfig buildTimeConfig, BuildProducer roleProducer, + BuildProducer clusterRoleProducer, BuildProducer roleBindingProducer, KubernetesConfigRecorder recorder) { if (buildTimeConfig.secretsEnabled) { - roleProducer.produce(new KubernetesRoleBuildItem("view-secrets", Collections.singletonList( - new KubernetesRoleBuildItem.PolicyRule( - Collections.singletonList(""), - Collections.singletonList("secrets"), - List.of("get"))))); - roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view-secrets", false)); + SecretsRoleConfig roleConfig = buildTimeConfig.secretsRoleConfig; + String roleName = roleConfig.name; + if (roleConfig.generate) { + if (roleConfig.clusterWide) { + clusterRoleProducer.produce(new KubernetesClusterRoleBuildItem(roleName, + POLICY_RULE_FOR_ROLE, + ANY_TARGET)); + } else { + roleProducer.produce(new KubernetesRoleBuildItem(roleName, + roleConfig.namespace.orElse(null), + POLICY_RULE_FOR_ROLE, + ANY_TARGET)); + } + } + + roleBindingProducer.produce(new KubernetesRoleBindingBuildItem(roleName, roleConfig.clusterWide)); } recorder.warnAboutSecrets(config, buildTimeConfig); diff --git a/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/KubernetesConfigBuildTimeConfig.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/KubernetesConfigBuildTimeConfig.java index cfe9f11d85a734..528c68c6eeb5c9 100644 --- a/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/KubernetesConfigBuildTimeConfig.java +++ b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/KubernetesConfigBuildTimeConfig.java @@ -12,4 +12,9 @@ public class KubernetesConfigBuildTimeConfig { */ @ConfigItem(name = "secrets.enabled", defaultValue = "false") public boolean secretsEnabled; + + /** + * Role configuration to generate if the "secrets-enabled" property is true. + */ + public SecretsRoleConfig secretsRoleConfig; } diff --git a/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/SecretsRoleConfig.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/SecretsRoleConfig.java new file mode 100644 index 00000000000000..c7cc615aadd978 --- /dev/null +++ b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/config/runtime/SecretsRoleConfig.java @@ -0,0 +1,34 @@ +package io.quarkus.kubernetes.config.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class SecretsRoleConfig { + + /** + * The name of the role. + */ + @ConfigItem(defaultValue = "view-secrets") + public String name; + + /** + * The namespace of the role. + */ + @ConfigItem + public Optional namespace; + + /** + * Whether the role is cluster wide or not. By default, it's not a cluster wide role. + */ + @ConfigItem(defaultValue = "false") + public boolean clusterWide; + + /** + * If the current role is meant to be generated or not. If not, it will only be used to generate the role binding resource. + */ + @ConfigItem(defaultValue = "true") + public boolean generate; +} 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 2c7ccf92ebcb44..fcfa770c0a3e55 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 @@ -32,6 +32,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -46,6 +47,7 @@ import io.quarkus.kubernetes.spi.KubernetesResourceMetadataBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class KindProcessor { @@ -111,6 +113,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot) { @@ -120,7 +124,7 @@ public List createDecorators(ApplicationInfoBuildItem applic livenessPath, readinessPath, startupPath, - roles, roleBindings, customProjectRoot); + roles, clusterRoles, serviceAccounts, roleBindings, customProjectRoot); } @BuildStep 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 ad8e89775d7057..da5193dac411a0 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 @@ -29,6 +29,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -43,6 +44,7 @@ import io.quarkus.kubernetes.spi.KubernetesResourceMetadataBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class MinikubeProcessor { @@ -107,6 +109,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot) { @@ -116,6 +120,6 @@ public List createDecorators(ApplicationInfoBuildItem applic livenessPath, readinessPath, startupPath, - roles, roleBindings, customProjectRoot); + roles, clusterRoles, serviceAccounts, roleBindings, customProjectRoot); } } diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesClusterRoleBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesClusterRoleBuildItem.java new file mode 100644 index 00000000000000..a1bf4655f95258 --- /dev/null +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesClusterRoleBuildItem.java @@ -0,0 +1,43 @@ +package io.quarkus.kubernetes.spi; + +import java.util.List; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Produce this build item to request the Kubernetes extension to generate + * a Kubernetes {@code ClusterRole} resource. + */ +public final class KubernetesClusterRoleBuildItem extends MultiBuildItem { + /** + * Name of the generated {@code ClusterRole} resource. + */ + private final String name; + /** + * The {@code PolicyRule} resources for this {@code ClusterRole}. + */ + private final List rules; + + /** + * The target manifest that should include this role. + */ + private final String target; + + public KubernetesClusterRoleBuildItem(String name, List rules, String target) { + this.name = name; + this.rules = rules; + this.target = target; + } + + public String getName() { + return name; + } + + public List getRules() { + return rules; + } + + public String getTarget() { + return target; + } +} diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java index 015f9e4dc40097..0e220489348bd4 100644 --- a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java @@ -1,5 +1,8 @@ package io.quarkus.kubernetes.spi; +import java.util.Collections; +import java.util.Map; + import io.quarkus.builder.item.MultiBuildItem; /** @@ -17,17 +20,22 @@ public final class KubernetesRoleBindingBuildItem extends MultiBuildItem { */ private final String name; /** - * Name of the bound role. - */ - private final String role; - /** - * If {@code true}, the binding refers to a {@code ClusterRole}, otherwise to a namespaced {@code Role}. + * RoleRef configuration. */ - private final boolean clusterWide; + private final RoleRef roleRef; /** * The target manifest that should include this role. */ private final String target; + /** + * The target subjects. + */ + private final Subject[] subjects; + + /** + * The labels of the cluster role resource. + */ + private final Map labels; public KubernetesRoleBindingBuildItem(String role, boolean clusterWide) { this(null, role, clusterWide, null); @@ -38,25 +46,85 @@ public KubernetesRoleBindingBuildItem(String name, String role, boolean clusterW } public KubernetesRoleBindingBuildItem(String name, String role, boolean clusterWide, String target) { + this(name, target, Collections.emptyMap(), + new RoleRef(role, clusterWide), + new Subject("", "ServiceAccount", name, null)); + } + + public KubernetesRoleBindingBuildItem(String name, String target, Map labels, RoleRef roleRef, + Subject... subjects) { this.name = name; - this.role = role; - this.clusterWide = clusterWide; this.target = target; + this.labels = labels; + this.roleRef = roleRef; + this.subjects = subjects; } public String getName() { return this.name; } - public String getRole() { - return this.role; + public String getTarget() { + return target; } - public boolean isClusterWide() { - return clusterWide; + public Map getLabels() { + return labels; } - public String getTarget() { - return target; + public RoleRef getRoleRef() { + return roleRef; + } + + public Subject[] getSubjects() { + return subjects; + } + + public static final class RoleRef { + private final boolean clusterWide; + private final String name; + + public RoleRef(String name, boolean clusterWide) { + this.name = name; + this.clusterWide = clusterWide; + } + + public boolean isClusterWide() { + return clusterWide; + } + + public String getName() { + return name; + } + } + + public static final class Subject { + private final String apiGroup; + private final String kind; + private final String name; + private final String namespace; + + public Subject(String apiGroup, String kind, String name, String namespace) { + this.apiGroup = apiGroup; + this.kind = kind; + this.name = name; + this.namespace = namespace; + } + + public String getApiGroup() { + return apiGroup; + } + + public String getKind() { + return kind; + } + + public String getName() { + return name; + } + + public String getNamespace() { + return namespace; + } } } diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java index c102250bb24844..c22c82410cc78c 100644 --- a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java @@ -15,6 +15,10 @@ public final class KubernetesRoleBuildItem extends MultiBuildItem { * Name of the generated {@code Role} resource. */ private final String name; + /** + * Namespace of the generated {@code Role} resource. + */ + private final String namespace; /** * The {@code PolicyRule} resources for this {@code Role}. */ @@ -30,7 +34,12 @@ public KubernetesRoleBuildItem(String name, List rules) { } public KubernetesRoleBuildItem(String name, List rules, String target) { + this(name, null, rules, target); + } + + public KubernetesRoleBuildItem(String name, String namespace, List rules, String target) { this.name = name; + this.namespace = namespace; this.rules = rules; this.target = target; } @@ -39,6 +48,10 @@ public String getName() { return name; } + public String getNamespace() { + return namespace; + } + public List getRules() { return rules; } @@ -46,48 +59,4 @@ public List getRules() { public String getTarget() { return target; } - - /** - * Corresponds directly to the Kubernetes {@code PolicyRule} resource. - */ - public static final class PolicyRule { - private final List apiGroups; - private final List nonResourceURLs; - private final List resourceNames; - private final List resources; - private final List verbs; - - public PolicyRule(List apiGroups, List resources, List verbs) { - this(apiGroups, null, null, resources, verbs); - } - - public PolicyRule(List apiGroups, List nonResourceURLs, List resourceNames, - List resources, List verbs) { - this.apiGroups = apiGroups; - this.nonResourceURLs = nonResourceURLs; - this.resourceNames = resourceNames; - this.resources = resources; - this.verbs = verbs; - } - - public List getApiGroups() { - return apiGroups; - } - - public List getNonResourceURLs() { - return nonResourceURLs; - } - - public List getResourceNames() { - return resourceNames; - } - - public List getResources() { - return resources; - } - - public List getVerbs() { - return verbs; - } - } } diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesServiceAccountBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesServiceAccountBuildItem.java new file mode 100644 index 00000000000000..0a89b0bdc74fd1 --- /dev/null +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesServiceAccountBuildItem.java @@ -0,0 +1,60 @@ +package io.quarkus.kubernetes.spi; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Produce this build item to request the Kubernetes extension to generate + * a Kubernetes {@code ServiceAccount} resource. + */ +public final class KubernetesServiceAccountBuildItem extends MultiBuildItem { + /** + * Name of the generated {@code ServiceAccount} resource. + */ + private final String name; + /** + * Namespace of the generated {@code ServiceAccount} resource. + */ + private final String namespace; + /** + * Labels of the generated {@code ServiceAccount} resource. + */ + private final Map labels; + + /** + * If true, this service account will be used in the generated Deployment resources. + */ + private final boolean useAsDefault; + + /** + * With empty parameters, it will generate a service account with the same name that the deployment. + */ + public KubernetesServiceAccountBuildItem(boolean useAsDefault) { + this(null, null, Collections.emptyMap(), useAsDefault); + } + + public KubernetesServiceAccountBuildItem(String name, String namespace, Map labels, boolean useAsDefault) { + this.name = name; + this.namespace = namespace; + this.labels = labels; + this.useAsDefault = useAsDefault; + } + + public String getName() { + return this.name; + } + + public String getNamespace() { + return namespace; + } + + public Map getLabels() { + return labels; + } + + public boolean isUseAsDefault() { + return useAsDefault; + } +} diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/PolicyRule.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/PolicyRule.java new file mode 100644 index 00000000000000..4dd68789bbd3e7 --- /dev/null +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/PolicyRule.java @@ -0,0 +1,47 @@ +package io.quarkus.kubernetes.spi; + +import java.util.List; + +/** + * Corresponds directly to the Kubernetes {@code PolicyRule} resource. + */ +public class PolicyRule { + private final List apiGroups; + private final List nonResourceURLs; + private final List resourceNames; + private final List resources; + private final List verbs; + + public PolicyRule(List apiGroups, List resources, List verbs) { + this(apiGroups, null, null, resources, verbs); + } + + public PolicyRule(List apiGroups, List nonResourceURLs, List resourceNames, + List resources, List verbs) { + this.apiGroups = apiGroups; + this.nonResourceURLs = nonResourceURLs; + this.resourceNames = resourceNames; + this.resources = resources; + this.verbs = verbs; + } + + public List getApiGroups() { + return apiGroups; + } + + public List getNonResourceURLs() { + return nonResourceURLs; + } + + public List getResourceNames() { + return resourceNames; + } + + public List getResources() { + return resources; + } + + public List getVerbs() { + return verbs; + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java new file mode 100644 index 00000000000000..2074e4fd122f10 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java @@ -0,0 +1,48 @@ +package io.quarkus.kubernetes.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.CLUSTER_ROLE; +import static io.quarkus.kubernetes.deployment.Constants.RBAC_API_VERSION; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder; +import io.fabric8.kubernetes.api.model.rbac.PolicyRule; + +class AddClusterRoleResourceDecorator extends ResourceProvidingDecorator { + private final String deploymentName; + private final String name; + private final Map labels; + private final List rules; + + public AddClusterRoleResourceDecorator(String deploymentName, String name, Map labels, + List rules) { + this.deploymentName = deploymentName; + this.name = name; + this.labels = labels; + this.rules = rules; + } + + public void visit(KubernetesListBuilder list) { + if (contains(list, RBAC_API_VERSION, CLUSTER_ROLE, name)) { + return; + } + + Map roleLabels = new HashMap<>(); + roleLabels.putAll(labels); + getDeploymentMetadata(list, deploymentName) + .map(ObjectMeta::getLabels) + .ifPresent(roleLabels::putAll); + + list.addToItems(new ClusterRoleBuilder() + .withNewMetadata() + .withName(name) + .withLabels(roleLabels) + .endMetadata() + .withRules(rules)); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceToSubjectDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceToSubjectDecorator.java index bce191c7b5e0c7..b04dd6ef279575 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceToSubjectDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceToSubjectDecorator.java @@ -25,7 +25,9 @@ public AddNamespaceToSubjectDecorator(String name, String namespace) { @Override public void andThenVisit(SubjectFluent subject, ObjectMeta resourceMeta) { - subject.withNamespace(namespace); + if (!subject.hasNamespace()) { + subject.withNamespace(namespace); + } } @Override diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleBindingResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleBindingResourceDecorator.java new file mode 100644 index 00000000000000..7be1897199680a --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleBindingResourceDecorator.java @@ -0,0 +1,70 @@ +package io.quarkus.kubernetes.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.CLUSTER_ROLE; +import static io.quarkus.kubernetes.deployment.Constants.RBAC_API_GROUP; +import static io.quarkus.kubernetes.deployment.Constants.RBAC_API_VERSION; +import static io.quarkus.kubernetes.deployment.Constants.ROLE; +import static io.quarkus.kubernetes.deployment.Constants.ROLE_BINDING; + +import java.util.HashMap; +import java.util.Map; + +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.dekorate.utils.Strings; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.rbac.RoleBindingBuilder; +import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; + +public class AddRoleBindingResourceDecorator extends ResourceProvidingDecorator { + + private final String deploymentName; + private final String name; + private final Map labels; + private final KubernetesRoleBindingBuildItem.RoleRef roleRef; + private final KubernetesRoleBindingBuildItem.Subject[] subjects; + + public AddRoleBindingResourceDecorator(String deploymentName, String name, Map labels, + KubernetesRoleBindingBuildItem.RoleRef roleRef, + KubernetesRoleBindingBuildItem.Subject... subjects) { + this.deploymentName = deploymentName; + this.name = name; + this.labels = labels; + this.roleRef = roleRef; + this.subjects = subjects; + } + + public void visit(KubernetesListBuilder list) { + if (contains(list, RBAC_API_VERSION, ROLE_BINDING, name)) { + return; + } + + Map roleBindingLabels = new HashMap<>(); + roleBindingLabels.putAll(labels); + getDeploymentMetadata(list, deploymentName) + .map(ObjectMeta::getLabels) + .ifPresent(roleBindingLabels::putAll); + + RoleBindingBuilder builder = new RoleBindingBuilder() + .withNewMetadata() + .withName(name) + .withLabels(roleBindingLabels) + .endMetadata() + .withNewRoleRef() + .withKind(roleRef.isClusterWide() ? CLUSTER_ROLE : ROLE) + .withName(roleRef.getName()) + .withApiGroup(RBAC_API_GROUP) + .endRoleRef(); + + for (KubernetesRoleBindingBuildItem.Subject subject : subjects) { + builder.addNewSubject() + .withApiGroup(subject.getApiGroup()) + .withKind(subject.getKind()) + .withName(Strings.defaultIfEmpty(subject.getName(), deploymentName)) + .withNamespace(subject.getNamespace()) + .endSubject(); + } + + list.addToItems(builder.build()); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java index cb4dfdec93f168..752efe7fd2b035 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java @@ -1,45 +1,51 @@ package io.quarkus.kubernetes.deployment; -import java.util.stream.Collectors; +import static io.quarkus.kubernetes.deployment.Constants.RBAC_API_VERSION; +import static io.quarkus.kubernetes.deployment.Constants.ROLE; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.api.model.rbac.PolicyRuleBuilder; +import io.fabric8.kubernetes.api.model.rbac.PolicyRule; import io.fabric8.kubernetes.api.model.rbac.RoleBuilder; -import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; class AddRoleResourceDecorator extends ResourceProvidingDecorator { private final String deploymentName; - private final KubernetesRoleBuildItem spec; + private final String name; + private final String namespace; + private final Map labels; + private final List rules; - public AddRoleResourceDecorator(String deploymentName, KubernetesRoleBuildItem buildItem) { + public AddRoleResourceDecorator(String deploymentName, String name, String namespace, Map labels, + List rules) { this.deploymentName = deploymentName; - this.spec = buildItem; + this.name = name; + this.namespace = namespace; + this.labels = labels; + this.rules = rules; } public void visit(KubernetesListBuilder list) { - ObjectMeta meta = getMandatoryDeploymentMetadata(list, deploymentName); - - if (contains(list, "rbac.authorization.k8s.io/v1", "Role", spec.getName())) { + if (contains(list, RBAC_API_VERSION, ROLE, name)) { return; } + Map roleLabels = new HashMap<>(); + roleLabels.putAll(labels); + getDeploymentMetadata(list, deploymentName) + .map(ObjectMeta::getLabels) + .ifPresent(roleLabels::putAll); + list.addToItems(new RoleBuilder() .withNewMetadata() - .withName(spec.getName()) - .withLabels(meta.getLabels()) + .withName(name) + .withNamespace(namespace) + .withLabels(roleLabels) .endMetadata() - .withRules( - spec.getRules() - .stream() - .map(it -> new PolicyRuleBuilder() - .withApiGroups(it.getApiGroups()) - .withNonResourceURLs(it.getNonResourceURLs()) - .withResourceNames(it.getResourceNames()) - .withResources(it.getResources()) - .withVerbs(it.getVerbs()) - .build()) - .collect(Collectors.toList()))); + .withRules(rules)); } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java new file mode 100644 index 00000000000000..b8fb1f0eb8dc48 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java @@ -0,0 +1,46 @@ +package io.quarkus.kubernetes.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.SERVICE_ACCOUNT; + +import java.util.HashMap; +import java.util.Map; + +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; + +public class AddServiceAccountResourceDecorator extends ResourceProvidingDecorator { + + private final String deploymentName; + private final String name; + private final String namespace; + private final Map labels; + + public AddServiceAccountResourceDecorator(String deploymentName, String name, String namespace, + Map labels) { + this.deploymentName = deploymentName; + this.name = name; + this.namespace = namespace; + this.labels = labels; + } + + public void visit(KubernetesListBuilder list) { + if (contains(list, "v1", SERVICE_ACCOUNT, name)) { + return; + } + + Map saLabels = new HashMap<>(); + saLabels.putAll(labels); + getDeploymentMetadata(list, deploymentName) + .map(ObjectMeta::getLabels) + .ifPresent(saLabels::putAll); + + list.addNewServiceAccountItem() + .withNewMetadata() + .withName(name) + .withNamespace(namespace) + .withLabels(saLabels) + .endMetadata() + .endServiceAccountItem(); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java new file mode 100644 index 00000000000000..7ac12a2e19f925 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java @@ -0,0 +1,29 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class ClusterRoleConfig { + + /** + * The name of the cluster role. + */ + @ConfigItem + Optional name; + + /** + * Labels to add into the ClusterRole resource. + */ + @ConfigItem + Map labels; + + /** + * Policy rules of the ClusterRole resource. + */ + @ConfigItem + Map policyRules; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java index 42993eab061cfc..47aa05a3db065b 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java @@ -9,12 +9,18 @@ public final class Constants { public static final String DEPLOYMENT = "Deployment"; public static final String JOB = "Job"; public static final String CRONJOB = "CronJob"; + public static final String ROLE = "Role"; + public static final String CLUSTER_ROLE = "ClusterRole"; + public static final String ROLE_BINDING = "RoleBinding"; + public static final String SERVICE_ACCOUNT = "ServiceAccount"; public static final String DEPLOYMENT_GROUP = "apps"; public static final String DEPLOYMENT_VERSION = "v1"; public static final String INGRESS = "Ingress"; public static final String BATCH_GROUP = "batch"; public static final String BATCH_VERSION = "v1"; public static final String JOB_API_VERSION = BATCH_GROUP + "/" + BATCH_VERSION; + public static final String RBAC_API_GROUP = "rbac.authorization.k8s.io"; + public static final String RBAC_API_VERSION = RBAC_API_GROUP + "/v1"; static final String DOCKER = "docker"; diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java index e6d17260dc07bd..40fff9e1317870 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java @@ -33,6 +33,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; @@ -45,6 +46,7 @@ import io.quarkus.kubernetes.spi.KubernetesProbePortNameBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class DevClusterHelper { @@ -70,6 +72,8 @@ public static List createDecorators(String clusterKind, Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot) { @@ -82,7 +86,7 @@ public static List createDecorators(String clusterKind, result.addAll(KubernetesCommonHelper.createDecorators(project, clusterKind, name, config, metricsConfiguration, annotations, labels, command, - port, livenessPath, readinessPath, startupPath, roles, roleBindings)); + port, livenessPath, readinessPath, startupPath, roles, clusterRoles, serviceAccounts, roleBindings)); image.ifPresent(i -> { result.add(new DecoratorBuildItem(clusterKind, new ApplyContainerImageDecorator(name, i.getImage()))); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java index de902e42db3d7b..d99751102f5b4e 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/InitTaskProcessor.java @@ -16,6 +16,7 @@ import io.quarkus.kubernetes.spi.KubernetesJobBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.PolicyRule; public class InitTaskProcessor { @@ -54,7 +55,7 @@ static void process( }); roles.produce(new KubernetesRoleBuildItem("view-jobs", Collections.singletonList( - new KubernetesRoleBuildItem.PolicyRule( + new PolicyRule( Collections.singletonList("batch"), Collections.singletonList("jobs"), List.of("get"))), 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 a1980bde84f5f9..940551d441e451 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 @@ -223,6 +223,12 @@ public class KnativeConfig implements PlatformConfiguration { @ConfigItem ResourcesConfig resources; + /** + * RBAC configuration + */ + @ConfigItem + RbacConfig rbac; + /** * If true, the 'app.kubernetes.io/version' label will be part of the selectors of Service and Deployment */ @@ -522,4 +528,9 @@ public SecurityContextConfig getSecurityContext() { public boolean isIdempotent() { return idempotent; } + + @Override + public RbacConfig getRbacConfig() { + return rbac; + } } 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 02e174e2257e33..8663ed5830e39c 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 @@ -52,6 +52,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -63,6 +64,7 @@ import io.quarkus.kubernetes.spi.KubernetesResourceMetadataBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class KnativeProcessor { @@ -143,6 +145,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupProbePath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot, List targets) { @@ -158,7 +162,8 @@ public List createDecorators(ApplicationInfoBuildItem applic packageConfig); Optional port = KubernetesCommonHelper.getPort(ports, config, "http"); result.addAll(KubernetesCommonHelper.createDecorators(project, KNATIVE, name, config, metricsConfiguration, annotations, - labels, command, port, livenessPath, readinessPath, startupProbePath, roles, roleBindings)); + labels, command, port, livenessPath, readinessPath, startupProbePath, + roles, clusterRoles, serviceAccounts, roleBindings)); image.ifPresent(i -> { result.add(new DecoratorBuildItem(KNATIVE, new ApplyContainerImageDecorator(name, i.getImage()))); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 2a7b4cd40b7a49..4907d451c47081 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -14,6 +14,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -22,6 +23,8 @@ import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + import io.dekorate.kubernetes.config.Annotation; import io.dekorate.kubernetes.config.ConfigMapVolumeBuilder; import io.dekorate.kubernetes.config.EnvBuilder; @@ -44,9 +47,7 @@ import io.dekorate.kubernetes.decorator.AddMountDecorator; import io.dekorate.kubernetes.decorator.AddPvcVolumeDecorator; import io.dekorate.kubernetes.decorator.AddReadinessProbeDecorator; -import io.dekorate.kubernetes.decorator.AddRoleBindingResourceDecorator; import io.dekorate.kubernetes.decorator.AddSecretVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddServiceAccountResourceDecorator; import io.dekorate.kubernetes.decorator.AddStartupProbeDecorator; import io.dekorate.kubernetes.decorator.ApplicationContainerDecorator; import io.dekorate.kubernetes.decorator.ApplyArgsDecorator; @@ -71,6 +72,8 @@ import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; 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.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; @@ -78,6 +81,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; @@ -89,6 +93,7 @@ import io.quarkus.kubernetes.spi.KubernetesProbePortNameBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class KubernetesCommonHelper { @@ -188,6 +193,8 @@ public static List createDecorators(Optional projec Optional readinessProbePath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings) { List result = new ArrayList<>(); @@ -208,27 +215,147 @@ public static List createDecorators(Optional projec } //Handle RBAC - roleBindings = roleBindings.stream() - .filter(roleBinding -> roleBinding.getTarget() == null || roleBinding.getTarget().equals(target)) - .collect(Collectors.toList()); - roles = roles.stream().filter(role -> role.getTarget() == null || role.getTarget().equals(target)) - .collect(Collectors.toList()); + result.addAll(createRbacDecorators(name, target, config, roles, clusterRoles, serviceAccounts, roleBindings)); + return result; + } - if (!roleBindings.isEmpty()) { - result.add(new DecoratorBuildItem(target, new ApplyServiceAccountNameDecorator(name, name))); - result.add(new DecoratorBuildItem(target, new AddServiceAccountResourceDecorator(name))); - roles.forEach(r -> result.add(new DecoratorBuildItem(target, new AddRoleResourceDecorator(name, r)))); - roleBindings.forEach(rb -> { - String rbName = Strings.isNotNullOrEmpty(rb.getName()) ? rb.getName() : name; - result.add(new DecoratorBuildItem(target, - new AddRoleBindingResourceDecorator(rbName, name, rb.getRole(), - rb.isClusterWide() ? AddRoleBindingResourceDecorator.RoleKind.ClusterRole - : AddRoleBindingResourceDecorator.RoleKind.Role))); - labels.forEach(l -> { - result.add(new DecoratorBuildItem(target, - new AddLabelDecorator(rb.getName(), l.getKey(), l.getValue(), "RoleBinding"))); - }); - }); + private static Collection createRbacDecorators(String name, String target, PlatformConfiguration config, + List rolesFromExtensions, + List clusterRolesFromExtensions, + List serviceAccountsFromExtensions, + List roleBindingsFromExtensions) { + List result = new ArrayList<>(); + + // Add roles from extensions + for (KubernetesRoleBuildItem role : rolesFromExtensions) { + if (role.getTarget() == null || role.getTarget().equals(target)) { + result.add(new DecoratorBuildItem(target, new AddRoleResourceDecorator(name, + role.getName(), + role.getNamespace(), + Collections.emptyMap(), + role.getRules() + .stream() + .map(it -> new PolicyRuleBuilder() + .withApiGroups(it.getApiGroups()) + .withNonResourceURLs(it.getNonResourceURLs()) + .withResourceNames(it.getResourceNames()) + .withResources(it.getResources()) + .withVerbs(it.getVerbs()) + .build()) + .collect(Collectors.toList())))); + } + } + + // Add roles from configuration + for (Map.Entry roleFromConfig : config.getRbacConfig().roles.entrySet()) { + RoleConfig role = roleFromConfig.getValue(); + String roleName = role.name.orElse(roleFromConfig.getKey()); + result.add(new DecoratorBuildItem(target, new AddRoleResourceDecorator(name, + roleName, + role.namespace.orElse(null), + role.labels, + toPolicyRulesList(role.policyRules)))); + } + + // Add cluster roles from configuration + for (Map.Entry clusterRoleFromConfig : config.getRbacConfig().clusterRoles.entrySet()) { + ClusterRoleConfig clusterRole = clusterRoleFromConfig.getValue(); + String clusterRoleName = clusterRole.name.orElse(clusterRoleFromConfig.getKey()); + result.add(new DecoratorBuildItem(target, new AddClusterRoleResourceDecorator(name, + clusterRoleName, + clusterRole.labels, + toPolicyRulesList(clusterRole.policyRules)))); + } + + // Add cluster roles from extensions + for (KubernetesClusterRoleBuildItem role : clusterRolesFromExtensions) { + if (role.getTarget() == null || role.getTarget().equals(target)) { + result.add(new DecoratorBuildItem(target, new AddClusterRoleResourceDecorator(name, + role.getName(), + Collections.emptyMap(), + role.getRules() + .stream() + .map(it -> new PolicyRuleBuilder() + .withApiGroups(it.getApiGroups()) + .withNonResourceURLs(it.getNonResourceURLs()) + .withResourceNames(it.getResourceNames()) + .withResources(it.getResources()) + .withVerbs(it.getVerbs()) + .build()) + .collect(Collectors.toList())))); + } + } + + // Add service account from extensions + String defaultServiceAccount = null; + for (KubernetesServiceAccountBuildItem sa : serviceAccountsFromExtensions) { + String saName = StringUtils.defaultString(sa.getName(), name); + result.add(new DecoratorBuildItem(target, new AddServiceAccountResourceDecorator(name, saName, + sa.getNamespace(), + sa.getLabels()))); + + if (sa.isUseAsDefault()) { + defaultServiceAccount = saName; + } + } + + // Add service account from configuration + for (Map.Entry sa : config.getRbacConfig().serviceAccounts.entrySet()) { + String saName = sa.getValue().name.orElse(sa.getKey()); + result.add(new DecoratorBuildItem(target, new AddServiceAccountResourceDecorator(name, saName, + sa.getValue().namespace.orElse(null), + sa.getValue().labels))); + + if (sa.getValue().useAsDefault.isPresent() && sa.getValue().useAsDefault.get()) { + defaultServiceAccount = saName; + } + } + + if (config.getServiceAccount().isPresent()) { + // use the one provided by the user + defaultServiceAccount = config.getServiceAccount().get(); + } + + // set service account in deployment resource + if (defaultServiceAccount != null) { + result.add(new DecoratorBuildItem(target, + new ApplyServiceAccountNameDecorator(name, defaultServiceAccount))); + } + + // Add role bindings from extensions + for (KubernetesRoleBindingBuildItem rb : roleBindingsFromExtensions) { + if (rb.getTarget() == null || rb.getTarget().equals(target)) { + result.add(new DecoratorBuildItem(target, new AddRoleBindingResourceDecorator(name, + Strings.isNotNullOrEmpty(rb.getName()) ? rb.getName() : name + "-" + rb.getRoleRef().getName(), + rb.getLabels(), + rb.getRoleRef(), + rb.getSubjects()))); + } + } + + // Add role bindings from configuration + for (Map.Entry rb : config.getRbacConfig().roleBindings.entrySet()) { + String rbName = rb.getValue().name.orElse(rb.getKey()); + RoleBindingConfig roleBinding = rb.getValue(); + + if (roleBinding.subjects.isEmpty()) { + throw new IllegalArgumentException( + "No Subjects have been set in the role binding configuration. You need to provide at least one."); + } + + List subjects = new ArrayList<>(); + for (Map.Entry s : roleBinding.subjects.entrySet()) { + String subjectName = s.getValue().name.orElse(s.getKey()); + SubjectConfig subject = s.getValue(); + subjects.add(new KubernetesRoleBindingBuildItem.Subject(subject.apiGroup.orElse(null), subject.kind, + subjectName, subject.namespace.orElse(null))); + } + + result.add(new DecoratorBuildItem(target, new AddRoleBindingResourceDecorator(name, + rbName, + roleBinding.labels, + new KubernetesRoleBindingBuildItem.RoleRef(roleBinding.roleName, roleBinding.clusterWide), + subjects.toArray(new KubernetesRoleBindingBuildItem.Subject[0])))); } return result; @@ -518,10 +645,6 @@ private static List createPodDecorators(Optional pr result.add(new DecoratorBuildItem(target, new AddHostAliasesDecorator(name, HostAliasConverter.convert(e)))); }); - config.getServiceAccount().ifPresent(s -> { - result.add(new DecoratorBuildItem(target, new ApplyServiceAccountNameDecorator(name, s))); - }); - config.getInitContainers().entrySet().forEach(e -> { result.add(new DecoratorBuildItem(target, new AddInitContainerDecorator(name, ContainerConverter.convert(e)))); }); @@ -809,4 +932,17 @@ private static Map verifyPorts(List ku } return result; } + + private static List toPolicyRulesList(Map policyRules) { + return policyRules.values() + .stream() + .map(it -> new PolicyRuleBuilder() + .withApiGroups(it.apiGroups.orElse(null)) + .withNonResourceURLs(it.nonResourceUrls.orElse(null)) + .withResourceNames(it.resourceNames.orElse(null)) + .withResources(it.resources.orElse(null)) + .withVerbs(it.verbs.orElse(null)) + .build()) + .collect(Collectors.toList()); + } } 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 b0553f8440a462..d5fd0a1782ad79 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 @@ -262,6 +262,11 @@ public enum DeploymentResourceKind { @ConfigItem ResourcesConfig resources; + /** + * RBAC configuration + */ + RbacConfig rbac; + /** * Ingress configuration */ @@ -566,6 +571,11 @@ public DeployStrategy getDeployStrategy() { return deployStrategy; } + @Override + public RbacConfig getRbacConfig() { + return rbac; + } + 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/OpenshiftConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java index 648653c7960ff0..6f731816ac42a2 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java @@ -327,6 +327,11 @@ public static enum DeploymentResourceKind { */ CronJobConfig cronJob; + /** + * RBAC configuration + */ + RbacConfig rbac; + public Optional getPartOf() { return partOf; } @@ -597,6 +602,11 @@ public DeployStrategy getDeployStrategy() { return deployStrategy; } + @Override + public RbacConfig getRbacConfig() { + return rbac; + } + public static boolean isOpenshiftBuildEnabled(ContainerImageConfig containerImageConfig, Capabilities capabilities) { boolean implicitlyEnabled = ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities) .filter(c -> 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 e7cf73b078c4d6..8b0300671b9fee 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 @@ -51,6 +51,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -65,6 +66,7 @@ import io.quarkus.kubernetes.spi.KubernetesResourceMetadataBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class OpenshiftProcessor { @@ -186,6 +188,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot, List targets) { @@ -204,7 +208,7 @@ public List createDecorators(ApplicationInfoBuildItem applic result.addAll(KubernetesCommonHelper.createDecorators(project, OPENSHIFT, name, config, metricsConfiguration, annotations, labels, command, - port, livenessPath, readinessPath, startupPath, roles, roleBindings)); + port, livenessPath, readinessPath, startupPath, roles, clusterRoles, serviceAccounts, roleBindings)); if (config.flavor == v3) { //Openshift 3.x doesn't recognize 'app.kubernetes.io/name', it uses 'app' instead. diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java index 84318ae6d93c97..94673530041b78 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java @@ -88,6 +88,8 @@ default String getConfigName() { Optional getAppConfigMap(); + RbacConfig getRbacConfig(); + SecurityContextConfig getSecurityContext(); boolean isIdempotent(); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PolicyRuleConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PolicyRuleConfig.java new file mode 100644 index 00000000000000..1dff5e6a3d22a8 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PolicyRuleConfig.java @@ -0,0 +1,40 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class PolicyRuleConfig { + /** + * API groups of the policy rule. + */ + @ConfigItem + Optional> apiGroups; + + /** + * Non resource URLs of the policy rule. + */ + @ConfigItem + Optional> nonResourceUrls; + + /** + * Resource names of the policy rule. + */ + @ConfigItem + Optional> resourceNames; + + /** + * Resources of the policy rule. + */ + @ConfigItem + Optional> resources; + + /** + * Verbs of the policy rule. + */ + @ConfigItem + Optional> verbs; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RbacConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RbacConfig.java new file mode 100644 index 00000000000000..e47638d2734422 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RbacConfig.java @@ -0,0 +1,34 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.List; +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class RbacConfig { + /** + * List of roles to generate. + */ + @ConfigItem + Map roles; + + /** + * List of cluster roles to generate. + */ + @ConfigItem + Map clusterRoles; + + /** + * List of service account resources to generate. + */ + @ConfigItem + Map serviceAccounts; + + /** + * List of role bindings to generate. + */ + @ConfigItem + Map roleBindings; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java new file mode 100644 index 00000000000000..7e4a37e244b888 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java @@ -0,0 +1,44 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class RoleBindingConfig { + + /** + * Name of the RoleBinding resource to be generated. If not provided, it will use the application name plus the role + * ref name. + */ + @ConfigItem + public Optional name; + + /** + * Labels to add into the RoleBinding resource. + */ + @ConfigItem + public Map labels; + + /** + * The name of the Role resource to use by the RoleRef element in the generated Role Binding resource. + * By default, it's "view" role name. + */ + @ConfigItem + public String roleName; + + /** + * If the Role sets in the `role-name` property is cluster wide or not. + * By default, it's "true". + */ + @ConfigItem(defaultValue = "true") + public boolean clusterWide; + + /** + * List of subjects elements to use in the generated RoleBinding resource. + */ + @ConfigItem + public Map subjects; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java new file mode 100644 index 00000000000000..5edb212b6a8160 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java @@ -0,0 +1,35 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class RoleConfig { + + /** + * The name of the role. + */ + @ConfigItem + Optional name; + + /** + * The namespace of the role. + */ + @ConfigItem + Optional namespace; + + /** + * Labels to add into the Role resource. + */ + @ConfigItem + Map labels; + + /** + * Policy rules of the Role resource. + */ + @ConfigItem + Map policyRules; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java new file mode 100644 index 00000000000000..9db74f2d0eb234 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java @@ -0,0 +1,35 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class ServiceAccountConfig { + + /** + * The name of the service account. + */ + @ConfigItem + Optional name; + + /** + * The namespace of the service account. + */ + @ConfigItem + Optional namespace; + + /** + * Labels of the service account. + */ + @ConfigItem + Map labels; + + /** + * If true, this service account will be used in the generated Deployment resource. + */ + @ConfigItem + Optional useAsDefault; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SubjectConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SubjectConfig.java new file mode 100644 index 00000000000000..76e2f543f9bc0d --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SubjectConfig.java @@ -0,0 +1,35 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class SubjectConfig { + + /** + * The "name" resource to use by the Subject element in the generated Role Binding resource. + */ + @ConfigItem + public Optional name; + + /** + * The "kind" resource to use by the Subject element in the generated Role Binding resource. + */ + @ConfigItem + public String kind; + + /** + * The "apiGroup" resource that matches with the "kind" property. By default, it's empty. + */ + @ConfigItem + public Optional apiGroup; + + /** + * The "namespace" resource to use by the Subject element in the generated Role Binding resource. + * By default, it will use the same as provided in the generated resources. + */ + @ConfigItem + public Optional namespace; +} 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 6a6aa9cf56dda9..7f392f0b3c997b 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 @@ -42,6 +42,7 @@ import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -56,6 +57,7 @@ import io.quarkus.kubernetes.spi.KubernetesResourceMetadataBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class VanillaKubernetesProcessor { @@ -136,6 +138,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot, List targets) { @@ -150,7 +154,8 @@ public List createDecorators(ApplicationInfoBuildItem applic packageConfig); Optional port = KubernetesCommonHelper.getPort(ports, config); result.addAll(KubernetesCommonHelper.createDecorators(project, KUBERNETES, name, config, metricsConfiguration, - annotations, labels, command, port, livenessPath, readinessPath, startupPath, roles, roleBindings)); + annotations, labels, command, port, livenessPath, readinessPath, startupPath, roles, clusterRoles, + serviceAccounts, roleBindings)); KubernetesConfig.DeploymentResourceKind deploymentKind = config.getDeploymentResourceKind(capabilities); if (deploymentKind != KubernetesConfig.DeploymentResourceKind.Deployment) { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsAndClusterRoleTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsAndClusterRoleTest.java new file mode 100644 index 00000000000000..d00a7c9d2deab1 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsAndClusterRoleTest.java @@ -0,0 +1,94 @@ +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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.rbac.ClusterRole; +import io.fabric8.kubernetes.api.model.rbac.PolicyRule; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; +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 KubernetesConfigWithSecretsAndClusterRoleTest { + + private static final String APP_NAME = "kubernetes-config-with-secrets-and-cluster-role"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-config", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil.deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + + assertThat(kubernetesList).anySatisfy(res -> { + assertThat(res).isInstanceOfSatisfying(ClusterRole.class, role -> { + assertThat(role.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("view-secrets"); + }); + + assertThat(role.getRules()).singleElement().satisfies(r -> { + assertThat(r).isInstanceOfSatisfying(PolicyRule.class, rule -> { + assertThat(rule.getApiGroups()).containsExactly(""); + assertThat(rule.getResources()).containsExactly("secrets"); + assertThat(rule.getVerbs()).containsExactly("get"); + }); + }); + }); + }); + + assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).hasSize(2) + .anySatisfy(res -> { + assertThat(res).isInstanceOfSatisfying(RoleBinding.class, roleBinding -> { + assertThat(roleBinding.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME + "-view-secrets"); + }); + + assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("ClusterRole"); + assertThat(roleBinding.getRoleRef().getName()).isEqualTo("view-secrets"); + + assertThat(roleBinding.getSubjects()).singleElement().satisfies(subject -> { + assertThat(subject.getKind()).isEqualTo("ServiceAccount"); + assertThat(subject.getName()).isEqualTo(APP_NAME); + }); + }); + }) + .anySatisfy(res -> { + assertThat(res).isInstanceOfSatisfying(RoleBinding.class, roleBinding -> { + assertThat(roleBinding.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME + "-view"); + }); + + assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("ClusterRole"); + assertThat(roleBinding.getRoleRef().getName()).isEqualTo("view"); + + assertThat(roleBinding.getSubjects()).singleElement().satisfies(subject -> { + assertThat(subject.getKind()).isEqualTo("ServiceAccount"); + assertThat(subject.getName()).isEqualTo(APP_NAME); + }); + }); + }); + } + +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java index 39b7a0f9922c0c..cbbf709c58bc3b 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java @@ -21,12 +21,14 @@ public class KubernetesConfigWithSecretsTest { + private static final String APP_NAME = "kubernetes-config-with-secrets"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-config-with-secrets") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-config-with-secrets.properties") + .withConfigurationResource(APP_NAME + ".properties") .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-config", Version.getVersion()))); @ProdBuildResults @@ -62,7 +64,7 @@ public void assertGeneratedResources() throws IOException { .anySatisfy(res -> { assertThat(res).isInstanceOfSatisfying(RoleBinding.class, roleBinding -> { assertThat(roleBinding.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-config-with-secrets-view-secrets"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-view-secrets"); }); assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("Role"); @@ -70,14 +72,14 @@ public void assertGeneratedResources() throws IOException { assertThat(roleBinding.getSubjects()).singleElement().satisfies(subject -> { assertThat(subject.getKind()).isEqualTo("ServiceAccount"); - assertThat(subject.getName()).isEqualTo("kubernetes-config-with-secrets"); + assertThat(subject.getName()).isEqualTo(APP_NAME); }); }); }) .anySatisfy(res -> { assertThat(res).isInstanceOfSatisfying(RoleBinding.class, roleBinding -> { assertThat(roleBinding.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-config-with-secrets-view"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-view"); }); assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("ClusterRole"); @@ -85,7 +87,7 @@ public void assertGeneratedResources() throws IOException { assertThat(roleBinding.getSubjects()).singleElement().satisfies(subject -> { assertThat(subject.getKind()).isEqualTo("ServiceAccount"); - assertThat(subject.getName()).isEqualTo("kubernetes-config-with-secrets"); + assertThat(subject.getName()).isEqualTo(APP_NAME); }); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndNamespaceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndNamespaceTest.java index a3ec55ccf60233..84a5a58049a13a 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndNamespaceTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndNamespaceTest.java @@ -24,12 +24,14 @@ public class KubernetesWithRbacAndNamespaceTest { + private static final String APP_NAME = "kubernetes-with-rbac-and-namespace"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-rbac-and-namespace") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-rbac-and-namespace.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), @@ -50,7 +52,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { assertThat(m.getLabels()).contains(entry("foo", "bar")); - assertThat(m.getName()).isEqualTo("kubernetes-with-rbac-and-namespace"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacTest.java new file mode 100644 index 00000000000000..cbdfdafd5b67b7 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacTest.java @@ -0,0 +1,117 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.rbac.ClusterRole; +import io.fabric8.kubernetes.api.model.rbac.Role; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; +import io.fabric8.kubernetes.api.model.rbac.Subject; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithRbacTest { + + private static final String APP_NAME = "kubernetes-with-rbac"; + private static final String APP_NAMESPACE = "projecta"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties"); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + + Deployment deployment = getDeploymentByName(kubernetesList, APP_NAME); + assertEquals(APP_NAMESPACE, deployment.getMetadata().getNamespace()); + + // pod-writer assertions + Role podWriterRole = getRoleByName(kubernetesList, "pod-writer"); + assertEquals(APP_NAMESPACE, podWriterRole.getMetadata().getNamespace()); + assertThat(podWriterRole.getRules()).satisfiesOnlyOnce(r -> { + assertThat(r.getResources()).containsExactly("pods"); + assertThat(r.getVerbs()).containsExactly("update"); + }); + + // pod-reader assertions + Role podReaderRole = getRoleByName(kubernetesList, "pod-reader"); + assertEquals("projectb", podReaderRole.getMetadata().getNamespace()); + assertThat(podReaderRole.getRules()).satisfiesOnlyOnce(r -> { + assertThat(r.getResources()).containsExactly("pods"); + assertThat(r.getVerbs()).containsExactly("get", "watch", "list"); + }); + + // secret-reader assertions + ClusterRole secretReaderRole = getClusterRoleByName(kubernetesList, "secret-reader"); + assertThat(secretReaderRole.getRules()).satisfiesOnlyOnce(r -> { + assertThat(r.getResources()).containsExactly("secrets"); + assertThat(r.getVerbs()).containsExactly("get", "watch", "list"); + }); + + // service account + ServiceAccount serviceAccount = getServiceAccountByName(kubernetesList, "user"); + assertEquals("projectc", serviceAccount.getMetadata().getNamespace()); + + // role binding + RoleBinding roleBinding = getRoleBindingByName(kubernetesList, "my-role-binding"); + assertEquals("pod-writer", roleBinding.getRoleRef().getName()); + assertEquals("Role", roleBinding.getRoleRef().getKind()); + Subject subject = roleBinding.getSubjects().get(0); + assertEquals("ServiceAccount", subject.getKind()); + assertEquals("user", subject.getName()); + assertEquals("projectc", subject.getNamespace()); + } + + private Deployment getDeploymentByName(List kubernetesList, String name) { + return getResourceByName(kubernetesList, Deployment.class, name); + } + + private Role getRoleByName(List kubernetesList, String roleName) { + return getResourceByName(kubernetesList, Role.class, roleName); + } + + private ClusterRole getClusterRoleByName(List kubernetesList, String clusterRoleName) { + return getResourceByName(kubernetesList, ClusterRole.class, clusterRoleName); + } + + private ServiceAccount getServiceAccountByName(List kubernetesList, String saName) { + return getResourceByName(kubernetesList, ServiceAccount.class, saName); + } + + private RoleBinding getRoleBindingByName(List kubernetesList, String rbName) { + return getResourceByName(kubernetesList, RoleBinding.class, rbName); + } + + private T getResourceByName(List kubernetesList, Class clazz, String name) { + Optional resource = kubernetesList.stream() + .filter(r -> r.getMetadata().getName().equals(name)) + .filter(clazz::isInstance) + .map(clazz::cast) + .findFirst(); + + assertTrue(resource.isPresent(), name + " resource not found!"); + return resource.get(); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndCustomRbacBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndCustomRbacBindingTest.java new file mode 100644 index 00000000000000..ff440ba7e2ccf6 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndCustomRbacBindingTest.java @@ -0,0 +1,66 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; +import io.fabric8.kubernetes.api.model.rbac.Subject; +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 WithKubernetesClientAndCustomRbacBindingTest { + + private static final String APP_NAME = "kubernetes-with-client-and-custom-rbac-binding"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .setRun(false) + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-client", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + + // The service account is not generated because we select another subject kind. + assertFalse(kubernetesList.stream().anyMatch(h -> "ServiceAccount".equals(h.getKind()))); + + assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).singleElement().satisfies(h -> { + assertThat(h.getMetadata().getName()).isEqualTo("my-role-binding-name"); + RoleBinding roleBinding = (RoleBinding) h; + // verify role ref + assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("Role"); + assertThat(roleBinding.getRoleRef().getName()).isEqualTo("my-role"); + + // verify subjects + assertThat(roleBinding.getSubjects()).isNotEmpty(); + Subject subject = roleBinding.getSubjects().get(0); + assertThat(subject.getKind()).isEqualTo("User"); + assertThat(subject.getName()).isEqualTo("jane"); + assertThat(subject.getNamespace()).isEqualTo("another-namespace"); + assertThat(subject.getApiGroup()).isEqualTo("rbac.authorization.k8s.io"); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientTest.java index 33bf95f6bc229c..b1c827dd5a307f 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; +import io.fabric8.kubernetes.api.model.rbac.Subject; import io.quarkus.builder.Version; import io.quarkus.maven.dependency.Dependency; import io.quarkus.test.LogFile; @@ -21,10 +23,12 @@ public class WithKubernetesClientTest { + private static final String APP_NAME = "kubernetes-with-client"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-client") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") @@ -58,11 +62,21 @@ public void assertGeneratedResources() throws IOException { .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); assertThat(kubernetesList).filteredOn(h -> "ServiceAccount".equals(h.getKind())).singleElement().satisfies(h -> { - assertThat(h.getMetadata().getName()).isEqualTo("kubernetes-with-client"); + assertThat(h.getMetadata().getName()).isEqualTo(APP_NAME); }); assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).singleElement().satisfies(h -> { - assertThat(h.getMetadata().getName()).isEqualTo("kubernetes-with-client-view"); + assertThat(h.getMetadata().getName()).isEqualTo(APP_NAME + "-view"); + RoleBinding roleBinding = (RoleBinding) h; + // verify role ref + assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("ClusterRole"); + assertThat(roleBinding.getRoleRef().getName()).isEqualTo("view"); + + // verify subjects + assertThat(roleBinding.getSubjects()).isNotEmpty(); + Subject subject = roleBinding.getSubjects().get(0); + assertThat(subject.getKind()).isEqualTo("ServiceAccount"); + assertThat(subject.getName()).isEqualTo(APP_NAME); }); } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets-and-cluster-role.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets-and-cluster-role.properties new file mode 100644 index 00000000000000..473cfafb230e28 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets-and-cluster-role.properties @@ -0,0 +1,4 @@ +quarkus.kubernetes-config.enabled=true +quarkus.kubernetes-config.secrets.enabled=true +quarkus.kubernetes-config.secrets-role-config.cluster-wide=true +quarkus.kubernetes-config.secrets=my-secret diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-and-custom-rbac-binding.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-and-custom-rbac-binding.properties new file mode 100644 index 00000000000000..11f5d4db528622 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-and-custom-rbac-binding.properties @@ -0,0 +1,8 @@ +quarkus.kubernetes-client.role-binding.name=my-role-binding-name +quarkus.kubernetes-client.role-binding.subject-kind=User +quarkus.kubernetes-client.role-binding.subject-api-group=rbac.authorization.k8s.io +quarkus.kubernetes-client.role-binding.subject-name=jane +quarkus.kubernetes-client.role-binding.subject-namespace=another-namespace + +quarkus.kubernetes-client.role-binding.role-name=my-role +quarkus.kubernetes-client.role-binding.cluster-wide=false \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac.properties new file mode 100644 index 00000000000000..5fe2b89c3c3a78 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac.properties @@ -0,0 +1,18 @@ +quarkus.kubernetes.namespace=projecta + +quarkus.kubernetes.rbac.roles.pod-writer.policy-rules.0.resources=pods +quarkus.kubernetes.rbac.roles.pod-writer.policy-rules.0.verbs=update + +quarkus.kubernetes.rbac.roles.pod-reader.namespace=projectb +quarkus.kubernetes.rbac.roles.pod-reader.policy-rules.0.resources=pods +quarkus.kubernetes.rbac.roles.pod-reader.policy-rules.0.verbs=get,watch,list + +quarkus.kubernetes.rbac.cluster-roles.secret-reader.policy-rules.0.resources=secrets +quarkus.kubernetes.rbac.cluster-roles.secret-reader.policy-rules.0.verbs=get,watch,list + +quarkus.kubernetes.rbac.service-accounts.user.namespace=projectc + +quarkus.kubernetes.rbac.role-bindings.my-role-binding.subjects.user.kind=ServiceAccount +quarkus.kubernetes.rbac.role-bindings.my-role-binding.subjects.user.namespace=projectc +quarkus.kubernetes.rbac.role-bindings.my-role-binding.role-name=pod-writer +quarkus.kubernetes.rbac.role-bindings.my-role-binding.cluster-wide=false \ No newline at end of file