From 01ba4659e7dafd7ccbc9747ac2534d556d7e9eed Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 15 Mar 2023 15:35:12 +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 | 58 ++++- .../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 | 246 ++++++++++++++++-- .../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 | 43 +++ .../kubernetes/deployment/RoleConfig.java | 35 +++ .../deployment/ServiceAccountConfig.java | 39 +++ .../kubernetes/deployment/SubjectConfig.java | 36 +++ .../VanillaKubernetesProcessor.java | 7 +- ...esConfigWithSecretsAndClusterRoleTest.java | 94 +++++++ .../KubernetesConfigWithSecretsTest.java | 14 +- .../KubernetesWithRbacAndNamespaceTest.java | 8 +- ...bernetesWithRbacAndServiceAccountTest.java | 97 +++++++ ...sWithRbacAndWithoutServiceAccountTest.java | 96 +++++++ .../KubernetesWithRbacFullTest.java | 117 +++++++++ .../KubernetesWithRbacSimpleTest.java | 95 +++++++ ...ernetesClientAndCustomRbacBindingTest.java | 66 +++++ .../kubernetes/WithKubernetesClientTest.java | 20 +- ...g-with-secrets-and-cluster-role.properties | 4 + ...-client-and-custom-rbac-binding.properties | 8 + ...s-with-rbac-and-service-account.properties | 10 + ...bac-and-without-service-account.properties | 7 + .../kubernetes-with-rbac-full.properties | 18 ++ .../kubernetes-with-rbac-simple.properties | 2 + 52 files changed, 1844 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/KubernetesWithRbacAndServiceAccountTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndWithoutServiceAccountTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacSimpleTest.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-and-service-account.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-and-without-service-account.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-full.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-simple.properties diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 9186346d280e8..21d969202c3d3 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -860,14 +860,68 @@ 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] ==== You can disable the RBAC resources generation using the property `quarkus.kubernetes-client.generate-rbac=false`. ==== +=== Generating RBAC resources + +In some scenarios, it's necessary to generate additional https://kubernetes.io/docs/reference/access-authn-authz/rbac/[RBAC] resources that are used by Kubernetes to grant or limit access to other resources. For example, in our use case, we are building https://kubernetes.io/docs/concepts/extend-kubernetes/operator/#operators-in-kubernetes[a Kubernetes operator] that needs to read the list of the installed deployments. To do this, we would need to assign a service account to our operator and link this service account with a role that grants access to the Deployment resources. Let's see how to do this using the `quarkus.kubernetes.rbac` properties: + +[source,properties] +---- +# Generate the Role resource with name "my-role" <1> +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.api-groups=extensions,apps +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.resources=deployments +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.verbs=list +---- + +<1> In this example, the role "my-role" will be generated with a policy rule to get the list of deployments. + +By default, if one role is configured, a RoleBinding resource will be generated as well to link this role with the ServiceAccount resource. + +Moreover, you can have more control over the RBAC resources to be generated: + +[source,properties] +---- +# Generate Role resource with name "my-role" <1> +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.api-groups=extensions,apps +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.resources=deployments +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.verbs=get,watch,list + +# Generate ServiceAccount resource with name "my-service-account" in namespace "my_namespace" <2> +quarkus.kubernetes.rbac.service-accounts.my-service-account.namespace=my_namespace + +# Bind Role "my-role" with ServiceAccount "my-service-account" <3> +quarkus.kubernetes.rbac.role-bindings.my-role-binding.subjects.my-service-account.kind=ServiceAccount +quarkus.kubernetes.rbac.role-bindings.my-role-binding.subjects.my-service-account.namespace=my_namespace +quarkus.kubernetes.rbac.role-bindings.my-role-binding.role-name=my-role +---- + +<1> In this example, the role "my-role" will be generated with the specified policy rules. +<2> Also, the service account "my-service-account" will be generated. +<3> And we can configure the generated RoleBinding resource by selecting the role to be used and the subject. + +Finally, we can also generate the cluster wide role resource of "ClusterRole" kind as follows: + +[source,properties] +---- +# Generate ClusterRole resource with name "my-cluster-role" <1> +quarkus.kubernetes.rbac.cluster-roles.my-cluster-role.policy-rules.0.api-groups=extensions,apps +quarkus.kubernetes.rbac.cluster-roles.my-cluster-role.policy-rules.0.resources=deployments +quarkus.kubernetes.rbac.cluster-roles.my-cluster-role.policy-rules.0.verbs=get,watch,list + +# Bind the ClusterRole "my-cluster-role" with the application service account +quarkus.kubernetes.rbac.role-bindings.my-role-binding.role-name=my-cluster-role <2> +---- + +<1> In this example, the cluster role "my-cluster-role" will be generated with the specified policy rules. +<2> As we have configured only one role, this property is not really necessary. + === Deploying to Minikube https://github.com/kubernetes/minikube[Minikube] is quite popular when a Kubernetes cluster is needed for development purposes. To make the deployment to Minikube 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 6b30a409b2c19..3d13ef439c3a8 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 bfdee6a2b635d..b07f97e5a0052 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 0000000000000..00946408f3f4f --- /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 7a86348c5bb36..8c4aa31830066 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 cfe9f11d85a73..528c68c6eeb5c 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 0000000000000..c7cc615aadd97 --- /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 2c7ccf92ebcb4..fcfa770c0a3e5 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 ad8e89775d705..da5193dac411a 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 0000000000000..a1bf4655f9525 --- /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 015f9e4dc4009..0e220489348bd 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 c102250bb2484..c22c82410cc78 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 0000000000000..0a89b0bdc74fd --- /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 0000000000000..4dd68789bbd3e --- /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 0000000000000..2074e4fd122f1 --- /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 bce191c7b5e0c..b04dd6ef27957 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 0000000000000..7be1897199680 --- /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 cb4dfdec93f16..752efe7fd2b03 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 0000000000000..b8fb1f0eb8dc4 --- /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 0000000000000..7ac12a2e19f92 --- /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 42993eab061cf..47aa05a3db065 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 e6d17260dc07b..40fff9e131787 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 de902e42db3d7..d99751102f5b4 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 a1980bde84f5f..940551d441e45 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 02e174e2257e3..8663ed5830e39 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 2a7b4cd40b7a4..140f890fc68cd 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 @@ -7,6 +7,7 @@ import static io.quarkus.kubernetes.deployment.Constants.QUARKUS_ANNOTATIONS_BUILD_TIMESTAMP; import static io.quarkus.kubernetes.deployment.Constants.QUARKUS_ANNOTATIONS_COMMIT_ID; import static io.quarkus.kubernetes.deployment.Constants.QUARKUS_ANNOTATIONS_VCS_URL; +import static io.quarkus.kubernetes.deployment.Constants.SERVICE_ACCOUNT; import java.nio.file.Path; import java.time.ZoneOffset; @@ -14,6 +15,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 +24,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 +48,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 +73,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 +82,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 +94,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 +194,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 +216,196 @@ 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<>(); + Set roles = new HashSet<>(); + Set clusterRoles = new HashSet<>(); + + // 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)))); + + roles.add(roleName); + } + + // 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())))); + + roles.add(role.getName()); + } + } + + // 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)))); + clusterRoles.add(clusterRoleName); + } + + // 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())))); + clusterRoles.add(role.getName()); + } + } + + // Add service account from extensions: use the one provided by the user always + String defaultServiceAccount = config.getServiceAccount().orElse(null); + String defaultServiceAccountNamespace = config.getNamespace().orElse(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 == null) { + defaultServiceAccount = saName; + defaultServiceAccountNamespace = sa.getNamespace(); + } + } + + // 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().isUseAsDefault() || defaultServiceAccount == null) { + defaultServiceAccount = saName; + defaultServiceAccountNamespace = sa.getValue().namespace.orElse(null); + } + } + + // Prepare default configuration + String defaultRoleName = null; + boolean defaultClusterWide = false; + boolean requiresServiceAccount = false; + if (!roles.isEmpty()) { + // generate a role binding using this first role. + defaultRoleName = roles.iterator().next(); + } else if (!clusterRoles.isEmpty()) { + // generate a role binding using this first cluster role. + defaultClusterWide = true; + defaultRoleName = clusterRoles.iterator().next(); + } + + // 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(); + + List subjects = new ArrayList<>(); + if (roleBinding.subjects.isEmpty()) { + requiresServiceAccount = defaultServiceAccount == null; + subjects.add(new KubernetesRoleBindingBuildItem.Subject(null, SERVICE_ACCOUNT, + defaultIfEmpty(defaultServiceAccount, name), defaultServiceAccountNamespace)); + } else { + 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))); + } + } + + String roleName = roleBinding.roleName.orElse(defaultRoleName); + if (roleName == null) { + throw new IllegalStateException("No role has been set in the RoleBinding resource!"); + } + + boolean clusterWide = roleBinding.clusterWide.orElse(defaultClusterWide); + result.add(new DecoratorBuildItem(target, new AddRoleBindingResourceDecorator(name, + rbName, + roleBinding.labels, + new KubernetesRoleBindingBuildItem.RoleRef(roleName, clusterWide), + subjects.toArray(new KubernetesRoleBindingBuildItem.Subject[0])))); + } + + // generate a default role binding if none was set + if (defaultRoleName != null && config.getRbacConfig().roleBindings.isEmpty() && roleBindingsFromExtensions.isEmpty()) { + requiresServiceAccount = defaultServiceAccount == null; + result.add(new DecoratorBuildItem(target, new AddRoleBindingResourceDecorator(name, + name, + Collections.emptyMap(), + new KubernetesRoleBindingBuildItem.RoleRef(defaultRoleName, defaultClusterWide), + new KubernetesRoleBindingBuildItem.Subject(null, SERVICE_ACCOUNT, + defaultIfEmpty(defaultServiceAccount, name), + defaultServiceAccountNamespace)))); + } + + // generate service account if none is set, and it's required by other resources + if (defaultServiceAccount == null && requiresServiceAccount) { + // use the application name + defaultServiceAccount = name; + // and generate the resource + result.add(new DecoratorBuildItem(target, + new AddServiceAccountResourceDecorator(name, defaultServiceAccount, defaultServiceAccountNamespace, + Collections.emptyMap()))); + } + + // set service account in deployment resource + if (defaultServiceAccount != null) { + result.add(new DecoratorBuildItem(target, new ApplyServiceAccountNameDecorator(name, defaultServiceAccount))); } return result; @@ -518,10 +695,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 +982,25 @@ 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()); + } + + private static String defaultIfEmpty(String str, String defaultStr) { + if (str == null || str.length() == 0) { + return defaultStr; + } + + return str; + } } 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 b0553f8440a46..d5fd0a1782ad7 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 648653c7960ff..6f731816ac42a 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 e7cf73b078c4d..8b0300671b9fe 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 84318ae6d93c9..94673530041b7 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 0000000000000..1dff5e6a3d22a --- /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 0000000000000..e47638d273442 --- /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 0000000000000..e390ea2d649e9 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java @@ -0,0 +1,43 @@ +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 Optional roleName; + + /** + * If the Role sets in the `role-name` property is cluster wide or not. + */ + @ConfigItem + public Optional 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 0000000000000..5edb212b6a816 --- /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 0000000000000..af96a4d6e3680 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java @@ -0,0 +1,39 @@ +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; + + public boolean isUseAsDefault() { + return useAsDefault.orElse(false); + } +} 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 0000000000000..d41e6b127b548 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SubjectConfig.java @@ -0,0 +1,36 @@ +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. + * By default, it uses the "ServiceAccount" kind. + */ + @ConfigItem(defaultValue = "ServiceAccount") + 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 6a6aa9cf56dda..7f392f0b3c997 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 0000000000000..d00a7c9d2deab --- /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 39b7a0f9922c0..cbbf709c58bc3 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 a3ec55ccf6023..84a5a58049a13 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/KubernetesWithRbacAndServiceAccountTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndServiceAccountTest.java new file mode 100644 index 0000000000000..9f3150eb84bdf --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndServiceAccountTest.java @@ -0,0 +1,97 @@ +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.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 KubernetesWithRbacAndServiceAccountTest { + + private static final String APP_NAME = "kubernetes-with-rbac-and-service-account"; + private static final String SERVICE_ACCOUNT = "my-service-account"; + private static final String ROLE = "my-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"); + + @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); + assertThat(deployment.getSpec().getTemplate().getSpec().getServiceAccountName()).isEqualTo(SERVICE_ACCOUNT); + + // my-role assertions + Role myRole = getRoleByName(kubernetesList, ROLE); + assertThat(myRole.getRules()).satisfiesOnlyOnce(r -> { + assertThat(r.getApiGroups()).containsExactly("extensions", "apps"); + assertThat(r.getResources()).containsExactly("deployments"); + assertThat(r.getVerbs()).containsExactly("get", "watch", "list"); + }); + + // service account + ServiceAccount serviceAccount = getServiceAccountByName(kubernetesList, SERVICE_ACCOUNT); + assertThat(serviceAccount).isNotNull(); + + // role binding + RoleBinding roleBinding = getRoleBindingByName(kubernetesList, "my-role-binding"); + assertEquals("Role", roleBinding.getRoleRef().getKind()); + assertEquals(ROLE, roleBinding.getRoleRef().getName()); + Subject subject = roleBinding.getSubjects().get(0); + assertEquals("ServiceAccount", subject.getKind()); + assertEquals(SERVICE_ACCOUNT, subject.getName()); + } + + 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 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/KubernetesWithRbacAndWithoutServiceAccountTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndWithoutServiceAccountTest.java new file mode 100644 index 0000000000000..d5dc8d1a358ae --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacAndWithoutServiceAccountTest.java @@ -0,0 +1,96 @@ +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.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 KubernetesWithRbacAndWithoutServiceAccountTest { + + private static final String APP_NAME = "kubernetes-with-rbac-and-without-service-account"; + private static final String ROLE = "my-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"); + + @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); + assertThat(deployment.getSpec().getTemplate().getSpec().getServiceAccountName()).isEqualTo(APP_NAME); + + // my-role assertions + Role myRole = getRoleByName(kubernetesList, ROLE); + assertThat(myRole.getRules()).satisfiesOnlyOnce(r -> { + assertThat(r.getApiGroups()).containsExactly("extensions", "apps"); + assertThat(r.getResources()).containsExactly("deployments"); + assertThat(r.getVerbs()).containsExactly("get", "watch", "list"); + }); + + // service account + ServiceAccount serviceAccount = getServiceAccountByName(kubernetesList, APP_NAME); + assertThat(serviceAccount).isNotNull(); + + // role binding + RoleBinding roleBinding = getRoleBindingByName(kubernetesList, "my-role-binding"); + assertEquals("Role", roleBinding.getRoleRef().getKind()); + assertEquals(ROLE, roleBinding.getRoleRef().getName()); + Subject subject = roleBinding.getSubjects().get(0); + assertEquals("ServiceAccount", subject.getKind()); + assertEquals(APP_NAME, subject.getName()); + } + + 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 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/KubernetesWithRbacFullTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.java new file mode 100644 index 0000000000000..80ee1df06892c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.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 KubernetesWithRbacFullTest { + + private static final String APP_NAME = "kubernetes-with-rbac-full"; + 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/KubernetesWithRbacSimpleTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacSimpleTest.java new file mode 100644 index 0000000000000..663a8417503aa --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacSimpleTest.java @@ -0,0 +1,95 @@ +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.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 KubernetesWithRbacSimpleTest { + + private static final String APP_NAME = "kubernetes-with-rbac-simple"; + 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_NAME, deployment.getSpec().getTemplate().getSpec().getServiceAccountName()); + + // pod-writer assertions + Role podWriterRole = getRoleByName(kubernetesList, "pod-writer"); + assertThat(podWriterRole.getRules()).satisfiesOnlyOnce(r -> { + assertThat(r.getResources()).containsExactly("pods"); + assertThat(r.getVerbs()).containsExactly("update"); + }); + + // service account + ServiceAccount serviceAccount = getServiceAccountByName(kubernetesList, APP_NAME); + assertThat(serviceAccount).isNotNull(); + + // role binding + RoleBinding roleBinding = getRoleBindingByName(kubernetesList, APP_NAME); + assertEquals("pod-writer", roleBinding.getRoleRef().getName()); + assertEquals("Role", roleBinding.getRoleRef().getKind()); + Subject subject = roleBinding.getSubjects().get(0); + assertEquals("ServiceAccount", subject.getKind()); + assertEquals(APP_NAME, subject.getName()); + } + + 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 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 0000000000000..ff440ba7e2ccf --- /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 33bf95f6bc229..b1c827dd5a307 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 0000000000000..473cfafb230e2 --- /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 0000000000000..11f5d4db52862 --- /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-and-service-account.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-and-service-account.properties new file mode 100644 index 0000000000000..6d0fa36d1fac3 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-and-service-account.properties @@ -0,0 +1,10 @@ +# Generate Role resource with name "my-role" +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.api-groups=extensions,apps +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.resources=deployments +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.verbs=get,watch,list + +# Use the service account "my-service-account" in the application, this property will also trigger the ServiceAccount resource. +quarkus.kubernetes.service-account=my-service-account + +# Bind Role "my-role" with ServiceAccount "my-service-account" (as no subject has been selected by default) +quarkus.kubernetes.rbac.role-bindings.my-role-binding.role-name=my-role \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-and-without-service-account.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-and-without-service-account.properties new file mode 100644 index 0000000000000..5f657c4e896a1 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-and-without-service-account.properties @@ -0,0 +1,7 @@ +# Generate Role resource with name "my-role" +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.api-groups=extensions,apps +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.resources=deployments +quarkus.kubernetes.rbac.roles.my-role.policy-rules.0.verbs=get,watch,list + +# Bind Role "my-role" with ServiceAccount "my-service-account" (as no subject has been selected by default) +quarkus.kubernetes.rbac.role-bindings.my-role-binding.role-name=my-role \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-full.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-full.properties new file mode 100644 index 0000000000000..5fe2b89c3c3a7 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-full.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 diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-simple.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-simple.properties new file mode 100644 index 0000000000000..7dd80886b3084 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-rbac-simple.properties @@ -0,0 +1,2 @@ +quarkus.kubernetes.rbac.roles.pod-writer.policy-rules.0.resources=pods +quarkus.kubernetes.rbac.roles.pod-writer.policy-rules.0.verbs=update \ No newline at end of file