Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

@RBACRoleRef to assign existing cluster roles #865

Merged
merged 9 commits into from
Apr 25, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkiverse.operatorsdk.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface RBACCRoleRef {
metacosm marked this conversation as resolved.
Show resolved Hide resolved
String apiGroup() default "";
metacosm marked this conversation as resolved.
Show resolved Hide resolved

String kind() default "";
metacosm marked this conversation as resolved.
Show resolved Hide resolved

String name() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.quarkiverse.operatorsdk.annotations.AdditionalRBACRules;
import io.quarkiverse.operatorsdk.annotations.RBACCRoleRef;
import io.quarkiverse.operatorsdk.annotations.RBACRule;

public class Constants {
Expand All @@ -28,4 +29,5 @@ private Constants() {
public static final DotName OBJECT = DotName.createSimple(Object.class.getName());
public static final DotName ADDITIONAL_RBAC_RULES = DotName.createSimple(AdditionalRBACRules.class.getName());
public static final DotName RBAC_RULE = DotName.createSimple(RBACRule.class.getName());
public static final DotName RBAC_ROLE_REF = DotName.createSimple(RBACCRoleRef.class.getName());
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import static io.quarkiverse.operatorsdk.deployment.AddClusterRolesDecorator.getClusterRoleName;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand All @@ -19,6 +21,8 @@
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBindingBuilder;
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
import io.fabric8.kubernetes.api.model.rbac.RoleBindingBuilder;
import io.fabric8.kubernetes.api.model.rbac.RoleRef;
import io.fabric8.kubernetes.api.model.rbac.RoleRefBuilder;
import io.quarkiverse.operatorsdk.runtime.BuildTimeOperatorConfiguration;
import io.quarkiverse.operatorsdk.runtime.QuarkusControllerConfiguration;

Expand Down Expand Up @@ -74,14 +78,32 @@ private List<HasMetadata> bindingsFor(QuarkusControllerConfiguration<?> controll
if (controllerConfiguration.watchCurrentNamespace()) {
// create a RoleBinding that will be applied in the current namespace if watching only the current NS
itemsToAdd.add(createRoleBinding(roleBindingName, controllerName, serviceAccountName, null));
//add additional Role Bindings
controllerConfiguration.getAdditionalRBACRoleRefs().forEach(
roleRef -> itemsToAdd
.add(createRoleBinding(getRandomBindingName(roleBindingName), controllerName, serviceAccountName,
null, roleRef)));
} else if (controllerConfiguration.watchAllNamespaces()) {
itemsToAdd.add(createClusterRoleBinding(serviceAccountName, controllerName,
controllerName + "-cluster-role-binding", "watch all namespaces",
getClusterRoleName(controllerName)));
//add additional Role Bindings
controllerConfiguration.getAdditionalRBACRoleRefs().forEach(
roleRef -> itemsToAdd.add(createClusterRoleBinding(serviceAccountName, controllerName,
getRandomBindingName(controllerName + "-cluster-role-binding"),
"watch all namespaces", getClusterRoleName(controllerName), roleRef)));
} else {
// create a RoleBinding using either the provided deployment namespace or the desired watched namespace name
desiredWatchedNamespaces
.forEach(ns -> itemsToAdd.add(createRoleBinding(roleBindingName, controllerName, serviceAccountName, ns)));
.forEach(ns -> {
itemsToAdd.add(createRoleBinding(roleBindingName, controllerName, serviceAccountName, ns));
//add additional Role Bindings
controllerConfiguration.getAdditionalRBACRoleRefs()
.forEach(roleRef -> itemsToAdd
.add(createRoleBinding(getRandomBindingName(roleBindingName), controllerName,
serviceAccountName,
null, roleRef)));
});
}

return itemsToAdd;
Expand All @@ -91,37 +113,62 @@ public static String getRoleBindingName(String controllerName) {
return controllerName + "-role-binding";
}

public static String getRandomBindingName(String name) {
metacosm marked this conversation as resolved.
Show resolved Hide resolved
SecureRandom random = new SecureRandom();
byte bytes[] = new byte[8];
random.nextBytes(bytes);
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
String token = encoder.encodeToString(bytes);
return token + "-" + name;
}

private static RoleBinding createRoleBinding(String roleBindingName, String controllerName,
String serviceAccountName, String namespace) {
return createRoleBinding(roleBindingName, controllerName, serviceAccountName, namespace, new RoleRefBuilder()
.withApiGroup(RBAC_AUTHORIZATION_GROUP).withKind(CLUSTER_ROLE).withName(getClusterRoleName(controllerName))
.build());
}

private static RoleBinding createRoleBinding(String roleBindingName, String controllerName,
dloiacono marked this conversation as resolved.
Show resolved Hide resolved
String serviceAccountName, String namespace, RoleRef roleRef) {
final var nsMsg = (namespace == null ? "current" : "'" + namespace + "'") + " namespace";
log.infov("Creating ''{0}'' RoleBinding to be applied to {1}", roleBindingName, nsMsg);
return new RoleBindingBuilder()
.withNewMetadata()
.withName(roleBindingName)
.withNamespace(deployNamespace.orElse(namespace))
.endMetadata()
.withNewRoleRef(RBAC_AUTHORIZATION_GROUP, CLUSTER_ROLE, getClusterRoleName(controllerName))
.withRoleRef(roleRef)
.addNewSubject(null, SERVICE_ACCOUNT, serviceAccountName,
deployNamespace.orElse(null))
.build();
}

private static ClusterRoleBinding createClusterRoleBinding(String serviceAccountName,
String controllerName, String bindingName, String controllerConfMessage,
String clusterRoleName) {
String clusterRoleName, RoleRef roleRef) {
dloiacono marked this conversation as resolved.
Show resolved Hide resolved
outputWarningIfNeeded(controllerName, bindingName, controllerConfMessage);
final var ns = deployNamespace.orElse(null);
log.infov("Creating ''{0}'' ClusterRoleBinding to be applied to ''{1}'' namespace", bindingName, ns);
return new ClusterRoleBindingBuilder()
.withNewMetadata().withName(bindingName)
.endMetadata()
.withNewRoleRef(RBAC_AUTHORIZATION_GROUP, CLUSTER_ROLE, clusterRoleName)
.withRoleRef(roleRef)
.addNewSubject()
.withKind(SERVICE_ACCOUNT).withName(serviceAccountName).withNamespace(ns)
.endSubject()
.build();
}

private static ClusterRoleBinding createClusterRoleBinding(String serviceAccountName,
String controllerName, String bindingName, String controllerConfMessage,
String clusterRoleName) {
return createClusterRoleBinding(serviceAccountName, controllerName, bindingName, controllerConfMessage,
clusterRoleName, new RoleRefBuilder()
.withApiGroup(RBAC_AUTHORIZATION_GROUP).withKind(CLUSTER_ROLE).withName(clusterRoleName)
.build());
}

private static void outputWarningIfNeeded(String controllerName, String crBindingName, String controllerConfMessage) {
// the decorator can be called several times but we only want to output the warning once
if (deployNamespace.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.rbac.PolicyRule;
import io.fabric8.kubernetes.api.model.rbac.PolicyRuleBuilder;
import io.fabric8.kubernetes.api.model.rbac.RoleRef;
import io.fabric8.kubernetes.api.model.rbac.RoleRefBuilder;
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
Expand Down Expand Up @@ -219,6 +221,9 @@ static QuarkusControllerConfiguration createConfiguration(
// check if we have additional RBAC rules to handle
final var additionalRBACRules = extractAdditionalRBACRules(info);

// check if we have additional RBAC role refs to handle
final var additionalRBACRoleRefs = extractAdditionalRBACRoleRefs(info);

// extract the namespaces
// first check if we explicitly set the namespaces via the annotations
Set<String> namespaces = null;
Expand Down Expand Up @@ -273,7 +278,8 @@ static QuarkusControllerConfiguration createConfiguration(
finalFilter,
maxReconciliationInterval,
onAddFilter, onUpdateFilter, genericFilter, retryClass, retryConfigurationClass, rateLimiterClass,
rateLimiterConfigurationClass, dependentResources, null, additionalRBACRules, fieldManager, itemStore);
rateLimiterConfigurationClass, dependentResources, null, additionalRBACRules, additionalRBACRoleRefs,
fieldManager, itemStore);

if (hasDependents) {
dependentResourceInfos.forEach(dependent -> {
Expand Down Expand Up @@ -325,6 +331,32 @@ private static List<PolicyRule> extractAdditionalRBACRules(ClassInfo info) {
return additionalRBACRules;
}

private static List<RoleRef> extractAdditionalRBACRoleRefs(ClassInfo info) {
final List<RoleRef> additionalRBACRRoleRefs = new ArrayList<>();
final var rbacRoleRefAnnotation = info.declaredAnnotation(RBAC_ROLE_REF);
if (rbacRoleRefAnnotation != null) {
final var builder = new RoleRefBuilder();

builder.withApiGroup(ConfigurationUtils.annotationValueOrDefault(rbacRoleRefAnnotation,
"apiGroup",
AnnotationValue::asString,
() -> null));
builder.withKind(ConfigurationUtils.annotationValueOrDefault(rbacRoleRefAnnotation,
"kind",
AnnotationValue::asString,
() -> null));

builder.withName(ConfigurationUtils.annotationValueOrDefault(rbacRoleRefAnnotation,
"name",
AnnotationValue::asString,
() -> null));

additionalRBACRRoleRefs.add(builder.build());
}

return additionalRBACRRoleRefs;
}

private static PolicyRule extractRule(AnnotationInstance ruleAnnotation) {
final var builder = new PolicyRuleBuilder();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding;
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
import io.fabric8.kubernetes.api.model.rbac.RoleRef;
import io.quarkiverse.operatorsdk.common.ConfigurationUtils;
import io.quarkiverse.operatorsdk.common.DeserializedKubernetesResourcesBuildItem;
import io.quarkiverse.operatorsdk.common.FileUtils;
import io.quarkiverse.operatorsdk.deployment.AddClusterRolesDecorator;
import io.quarkiverse.operatorsdk.deployment.AddRoleBindingsDecorator;
import io.quarkiverse.operatorsdk.deployment.ControllerConfigurationsBuildItem;
import io.quarkiverse.operatorsdk.deployment.GeneratedCRDInfoBuildItem;
import io.quarkiverse.operatorsdk.deployment.helm.model.Chart;
Expand Down Expand Up @@ -58,6 +60,7 @@ public class HelmChartProcessor {
public static final String VALUES_YAML_FILENAME = "values.yaml";
public static final String CRD_DIR = "crds";
public static final String CRD_ROLE_BINDING_TEMPLATE_PATH = "/helm/crd-role-binding-template.yaml";
public static final String CRD_SECONDARY_ROLE_BINDING_TEMPLATE_PATH = "/helm/secondary-crd-role-binding-template.yaml";

@BuildStep
HelmTargetDirectoryBuildItem createRelatedDirectories(OutputTargetBuildItem outputTarget) {
Expand Down Expand Up @@ -100,6 +103,47 @@ void addPrimaryClusterRoleBindings(HelmTargetDirectoryBuildItem helmTargetDirect
});
}

@BuildStep
@Produce(ArtifactResultBuildItem.class)
void addSecondaryClusterRoleBindings(HelmTargetDirectoryBuildItem helmTargetDirectoryBuildItem,
ControllerConfigurationsBuildItem controllerConfigurations) {
final var controllerConfigs = controllerConfigurations.getControllerConfigs().values();

if (!controllerConfigs.isEmpty()) {
final String template;
try (InputStream file = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(CRD_SECONDARY_ROLE_BINDING_TEMPLATE_PATH)) {
if (file == null) {
throw new IllegalArgumentException(
"Template file " + CRD_SECONDARY_ROLE_BINDING_TEMPLATE_PATH + " doesn't exist");
}
template = new String(file.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalStateException(e);
}

final var templatesDir = helmTargetDirectoryBuildItem.getPathToTemplatesDir();

controllerConfigs.forEach(cc -> {
final String bindingName = AddRoleBindingsDecorator.getRandomBindingName(cc.getName());
final List<RoleRef> roleRefs = cc.getAdditionalRBACRoleRefs();
roleRefs
.forEach(roleRef -> {
try {
String res = Qute.fmt(template, Map.of(
"role-binding-name", bindingName,
"role-ref-kind", roleRef.getKind(),
"role-ref-api-group", roleRef.getApiGroup(),
"role-ref-name", roleRef.getName()));
Files.writeString(templatesDir.resolve(bindingName + "-secondary-crd-role-binding.yaml"), res);
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
});
}
}

@BuildStep
@Produce(ArtifactResultBuildItem.class)
void addClusterRolesForReconcilers(HelmTargetDirectoryBuildItem helmTargetDirectoryBuildItem,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{{ if eq $.Values.watchNamespaces "JOSDK_WATCH_CURRENT" }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {role-binding-name}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: quarkus
roleRef:
kind: {role-ref-kind}
apiGroup: {role-ref-api-group}
name: {role-ref-name}
subjects:
- kind: ServiceAccount
name: {{ $.Chart.Name }}
{{ else if eq $.Values.watchNamespaces "JOSDK_ALL_NAMESPACES" }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {role-binding-name}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: quarkus
roleRef:
kind: {role-ref-kind}
apiGroup: {role-ref-api-group}
name: {role-ref-name}
subjects:
- kind: ServiceAccount
name: {{ $.Chart.Name }}
namespace: {{ $.Release.Namespace }}
{{ else }}
{{ range $anamespace := ( split "," $.Values.watchNamespaces ) }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {role-binding-name}
namespace: {{ $anamespace }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: quarkus
roleRef:
kind: {role-ref-kind}
apiGroup: {role-ref-api-group}
name: {role-ref-name}
subjects:
- kind: ServiceAccount
name: {{ $.Chart.Name }}
namespace: {{ $.Release.Namespace }}
---
{{- end }}
{{- end }}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.quarkiverse.operatorsdk.annotations.RBACCRoleRef;
import io.quarkiverse.operatorsdk.annotations.RBACRule;
import io.quarkiverse.operatorsdk.annotations.RBACVerbs;

@ControllerConfiguration(name = SimpleReconciler.NAME)
@RBACRule(verbs = RBACVerbs.UPDATE, apiGroups = SimpleReconciler.CERTIFICATES_K8S_IO_GROUP, resources = SimpleReconciler.ADDITIONAL_UPDATE_RESOURCE)
@RBACRule(verbs = SimpleReconciler.SIGNERS_VERB, apiGroups = SimpleReconciler.CERTIFICATES_K8S_IO_GROUP, resources = SimpleReconciler.SIGNERS_RESOURCE, resourceNames = SimpleReconciler.SIGNERS_RESOURCE_NAMES)
@RBACCRoleRef(name = SimpleReconciler.ROLE_REF_NAME, apiGroup = SimpleReconciler.RBAC_AUTHORIZATION_GROUP, kind = SimpleReconciler.ROLE_REF_KIND)
metacosm marked this conversation as resolved.
Show resolved Hide resolved
public class SimpleReconciler implements Reconciler<SimpleCR> {

public static final String NAME = "simple";
Expand All @@ -18,6 +20,9 @@ public class SimpleReconciler implements Reconciler<SimpleCR> {
public static final String SIGNERS_VERB = "approve";
public static final String SIGNERS_RESOURCE = "signers";
public static final String SIGNERS_RESOURCE_NAMES = "kubernetes.io/kubelet-serving";
public static final String RBAC_AUTHORIZATION_GROUP = "rbac.authorization.k8s.io";
public static final String ROLE_REF_NAME = "system:auth-delegator";
public static final String ROLE_REF_KIND = "ClusterRole";

@Override
public UpdateControl<SimpleCR> reconcile(SimpleCR simpleCR, Context<SimpleCR> context) {
Expand Down
Loading
Loading