diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 9186346d280e8..7a33f0a02f11f 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -860,14 +860,69 @@ 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. +To ease the usage of the `kubernetes-client` extension, the `kubernetes` extension is going to generate a RoleBinding resource that binds a cluster role named "view" to the application ServiceAccount resource. It's important to note that the cluster role "view" won't be generated automatically, so it's expected that you have this cluster role with name "view" already installed in your cluster. + +On the other hand, you can fully customize the roles, subjects and role bindings to generate using the properties under `quarkus.kubernetes.rbac.role-bindings`, and if present, the `kubernetes-client` extension will use it and hence won't generate any RoleBinding resource. [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 3a379b6e24189..b04612791c9d5 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,7 @@ 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.spi.KubernetesRoleBindingBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.maven.dependency.ArtifactKey; public class KubernetesClientProcessor { @@ -69,6 +69,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,13 +107,13 @@ public void process(ApplicationIndexBuildItem applicationIndex, CombinedIndexBui BuildProducer reflectiveClasses, BuildProducer reflectiveHierarchies, BuildProducer ignoredJsonDeserializationClasses, - BuildProducer roleBindingProducer, - BuildProducer serviceProviderProducer) { + BuildProducer serviceProviderProducer, + BuildProducer kubernetesClientCapabilityProducer) { featureProducer.produce(new FeatureBuildItem(Feature.KUBERNETES_CLIENT)); - if (kubernetesClientConfig.generateRbac) { - roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view", true)); - } + + kubernetesClientCapabilityProducer + .produce(new KubernetesClientCapabilityBuildItem(kubernetesClientConfig.generateRbac)); // register fully (and not weakly) for reflection watchers, informers and custom resources final Set watchedClasses = new HashSet<>(); 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..679e99a1df121 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,7 +170,9 @@ public class KubernetesClientBuildConfig { public Optional noProxy; /** - * Enable the generation of the RBAC manifests. + * Enable the generation of the RBAC manifests. If enabled and no other role binding are provided using the properties + * `quarkus.kubernetes.rbac.`, it will generate a default role binding using the role "view" and the application + * service account. */ @ConfigItem(defaultValue = "true") public boolean generateRbac; diff --git a/extensions/kubernetes-client/spi/src/main/java/io/quarkus/kubernetes/client/spi/KubernetesClientCapabilityBuildItem.java b/extensions/kubernetes-client/spi/src/main/java/io/quarkus/kubernetes/client/spi/KubernetesClientCapabilityBuildItem.java new file mode 100644 index 0000000000000..b4283e3a2c0e5 --- /dev/null +++ b/extensions/kubernetes-client/spi/src/main/java/io/quarkus/kubernetes/client/spi/KubernetesClientCapabilityBuildItem.java @@ -0,0 +1,16 @@ +package io.quarkus.kubernetes.client.spi; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class KubernetesClientCapabilityBuildItem extends SimpleBuildItem { + + private final boolean generateRbac; + + public KubernetesClientCapabilityBuildItem(boolean generateRbac) { + this.generateRbac = generateRbac; + } + + public boolean isGenerateRbac() { + return generateRbac; + } +} 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..9a14a1760e512 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,22 @@ 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.KubernetesServiceAccountBuildItem; +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 +45,28 @@ public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecord public void handleAccessToSecrets(KubernetesConfigSourceConfig config, KubernetesConfigBuildTimeConfig buildTimeConfig, BuildProducer roleProducer, + BuildProducer clusterRoleProducer, + BuildProducer serviceAccountProducer, 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)); + } + } + + serviceAccountProducer.produce(new KubernetesServiceAccountBuildItem(true)); + 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 e23fa6be20d85..94a42ab4c9c5a 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 @@ -23,6 +23,7 @@ import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.util.ExecUtil; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.kubernetes.deployment.AddPortToKubernetesConfig; import io.quarkus.kubernetes.deployment.DevClusterHelper; import io.quarkus.kubernetes.deployment.KubernetesCommonHelper; @@ -32,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.KubernetesDeploymentTargetBuildItem; import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; @@ -46,6 +48,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 { @@ -97,6 +100,7 @@ public List createDecorators(ApplicationInfoBuildItem applic KubernetesConfig config, PackageConfig packageConfig, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, List initContainers, List jobs, List annotations, @@ -111,16 +115,16 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot) { return DevClusterHelper.createDecorators(KIND, applicationInfo, outputTarget, config, packageConfig, - metricsConfiguration, initContainers, jobs, annotations, labels, envs, baseImage, image, command, ports, - portName, - livenessPath, - readinessPath, - startupPath, - roles, roleBindings, customProjectRoot); + metricsConfiguration, kubernetesClientConfiguration, initContainers, jobs, annotations, labels, envs, + baseImage, image, command, ports, portName, + livenessPath, readinessPath, startupPath, + 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 44ad0bd7f3a7b..96343e7f69757 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 @@ -20,6 +20,7 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.kubernetes.deployment.AddPortToKubernetesConfig; import io.quarkus.kubernetes.deployment.DevClusterHelper; import io.quarkus.kubernetes.deployment.KubernetesCommonHelper; @@ -29,6 +30,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 +45,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 { @@ -93,6 +96,7 @@ public List createDecorators(ApplicationInfoBuildItem applic KubernetesConfig config, PackageConfig packageConfig, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, List initContainers, List jobs, List annotations, @@ -107,15 +111,15 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot) { return DevClusterHelper.createDecorators(MINIKUBE, applicationInfo, outputTarget, config, packageConfig, - metricsConfiguration, initContainers, jobs, annotations, labels, envs, baseImage, image, command, ports, - portName, - livenessPath, - readinessPath, - startupPath, - roles, roleBindings, customProjectRoot); + metricsConfiguration, kubernetesClientConfiguration, initContainers, jobs, annotations, labels, envs, + baseImage, image, command, ports, portName, + livenessPath, readinessPath, startupPath, + roles, clusterRoles, serviceAccounts, roleBindings, customProjectRoot); } } \ No newline at end of file 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 4a7d0ffe8055f..3e837624d5e7f 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 @@ -30,9 +30,11 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; 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 +47,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 { @@ -56,6 +59,7 @@ public static List createDecorators(String clusterKind, KubernetesConfig config, PackageConfig packageConfig, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, List initContainers, List jobs, List annotations, @@ -70,6 +74,8 @@ public static List createDecorators(String clusterKind, Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot) { @@ -80,9 +86,9 @@ public static List createDecorators(String clusterKind, packageConfig); Optional port = KubernetesCommonHelper.getPort(ports, config); result.addAll(KubernetesCommonHelper.createDecorators(project, clusterKind, name, config, - metricsConfiguration, + metricsConfiguration, kubernetesClientConfiguration, 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 5524fe655658a..15b6ab665ba25 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 @@ -48,10 +48,12 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.kubernetes.spi.ConfiguratorBuildItem; 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 +65,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 { @@ -132,6 +135,7 @@ public List createDecorators(ApplicationInfoBuildItem applic KnativeConfig config, PackageConfig packageConfig, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, List annotations, List labels, List envs, @@ -143,6 +147,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupProbePath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot, List targets) { @@ -157,8 +163,10 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional project = KubernetesCommonHelper.createProject(applicationInfo, customProjectRoot, outputTarget, 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)); + result.addAll(KubernetesCommonHelper.createDecorators(project, KNATIVE, name, config, + metricsConfiguration, kubernetesClientConfiguration, annotations, + 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 38ec88c14d0cd..d891739309df7 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; @@ -44,9 +46,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,13 +71,17 @@ 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; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; +import io.quarkus.kubernetes.spi.KubernetesClusterRoleBuildItem; import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; @@ -89,6 +93,7 @@ import io.quarkus.kubernetes.spi.KubernetesProbePortNameBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesServiceAccountBuildItem; public class KubernetesCommonHelper { @@ -96,6 +101,7 @@ public class KubernetesCommonHelper { private static final String OUTPUT_ARTIFACT_FORMAT = "%s%s.jar"; private static final String[] PROMETHEUS_ANNOTATION_TARGETS = { "Service", "Deployment", "DeploymentConfig" }; + private static final String DEFAULT_ROLE_NAME_VIEW = "view"; public static Optional createProject(ApplicationInfoBuildItem app, Optional customProjectRoot, OutputTargetBuildItem outputTarget, @@ -180,6 +186,7 @@ public static Map combinePorts(List ports public static List createDecorators(Optional project, String target, String name, PlatformConfiguration config, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, List annotations, List labels, Optional command, @@ -188,6 +195,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 +217,213 @@ 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, kubernetesClientConfiguration, 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, + Optional kubernetesClientConfiguration, + List rolesFromExtensions, + List clusterRolesFromExtensions, + List serviceAccountsFromExtensions, + List roleBindingsFromExtensions) { + List result = new ArrayList<>(); + boolean kubernetesClientRequiresRbacGeneration = kubernetesClientConfiguration + .map(KubernetesClientCapabilityBuildItem::isGenerateRbac).orElse(false); + 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())))); + } + } + + // 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())))); + } + } + + // 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 = defaultIfEmpty(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])))); + } + + // if no role bindings were created, then automatically create one if: + if (config.getRbacConfig().roleBindings.isEmpty()) { + if (defaultRoleName != null) { + // generate a default role binding if a default role name was configured + 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)))); + } else if (kubernetesClientRequiresRbacGeneration) { + // the property `quarkus.kubernetes-client.generate-rbac` is enabled + // and the kubernetes-client extension is present + requiresServiceAccount = true; + result.add(new DecoratorBuildItem(target, new AddRoleBindingResourceDecorator(name, + name + "-" + DEFAULT_ROLE_NAME_VIEW, + Collections.emptyMap(), + new KubernetesRoleBindingBuildItem.RoleRef(DEFAULT_ROLE_NAME_VIEW, true), + 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 +713,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 +1000,25 @@ private static Map verifyPorts(List ku } return result; } -} \ No newline at end of file + + 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 009e0f590296b..5bbb03e982892 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 @@ -46,11 +46,13 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.kubernetes.deployment.OpenshiftConfig.DeploymentResourceKind; import io.quarkus.kubernetes.spi.ConfiguratorBuildItem; 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 +67,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 { @@ -171,6 +174,7 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional fallbackRegistry, PackageConfig packageConfig, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, Capabilities capabilities, List initContainers, List jobs, @@ -186,6 +190,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot, List targets) { @@ -202,9 +208,9 @@ public List createDecorators(ApplicationInfoBuildItem applic packageConfig); Optional port = KubernetesCommonHelper.getPort(ports, config, config.route.targetPort); result.addAll(KubernetesCommonHelper.createDecorators(project, OPENSHIFT, name, config, - metricsConfiguration, + metricsConfiguration, kubernetesClientConfiguration, 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 f96f6de5007bd..031d4a9e9530f 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 @@ -38,10 +38,12 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem; import io.quarkus.kubernetes.spi.ConfiguratorBuildItem; 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 +58,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 { @@ -126,6 +129,7 @@ public List createDecorators(ApplicationInfoBuildItem applic OutputTargetBuildItem outputTarget, Capabilities capabilities, KubernetesConfig config, PackageConfig packageConfig, Optional metricsConfiguration, + Optional kubernetesClientConfiguration, List jobs, List initContainers, List annotations, @@ -136,6 +140,8 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional readinessPath, Optional startupPath, List roles, + List clusterRoles, + List serviceAccounts, List roleBindings, Optional customProjectRoot, List targets) { @@ -149,8 +155,10 @@ public List createDecorators(ApplicationInfoBuildItem applic Optional project = KubernetesCommonHelper.createProject(applicationInfo, customProjectRoot, outputTarget, 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)); + result.addAll(KubernetesCommonHelper.createDecorators(project, KUBERNETES, name, config, + metricsConfiguration, kubernetesClientConfiguration, 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..456b21d34c706 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsAndClusterRoleTest.java @@ -0,0 +1,79 @@ +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); + }); + }); + }); + } + +} 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..09a7bed0f9f8d 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 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/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-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