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..acf3e78e18a02 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,197 @@ 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 = null; + String defaultServiceAccountNamespace = 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 = true; + subjects.add(new KubernetesRoleBindingBuildItem.Subject(null, SERVICE_ACCOUNT, + defaultIfEmpty(defaultServiceAccount, config.getServiceAccount().orElse(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 = true; + result.add(new DecoratorBuildItem(target, new AddRoleBindingResourceDecorator(name, + name, + Collections.emptyMap(), + new KubernetesRoleBindingBuildItem.RoleRef(defaultRoleName, defaultClusterWide), + new KubernetesRoleBindingBuildItem.Subject(null, SERVICE_ACCOUNT, + defaultIfEmpty(defaultServiceAccount, config.getServiceAccount().orElse(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 = config.getServiceAccount().orElse(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 +696,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 +983,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