Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/v1alpha1/operatorconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const (

// DefaultAutoPauseTimeoutMinutes is the default idle timeout in minutes before a workspace is auto-paused
DefaultAutoPauseTimeoutMinutes int32 = 30

// BuildServiceAccountName is the dedicated SA used by build pods and token minting.
// Using a dedicated SA avoids collisions with the shared "pipeline" SA.
BuildServiceAccountName = "ado-build"
)

// ImagesConfig defines container image references used by the operator
Expand Down Expand Up @@ -575,6 +579,10 @@ type OperatorConfigStatus struct {

// JumpstarterAvailable indicates if Jumpstarter is available (explicitly configured or local CRDs detected)
JumpstarterAvailable bool `json:"jumpstarterAvailable,omitempty"`

// UserNamespacesSupported indicates if the cluster supports user namespaces in pods
// (SCC userNamespaceLevel field). When false, workspace pods use privileged mode.
UserNamespacesSupported bool `json:"userNamespacesSupported,omitempty"`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// +kubebuilder:object:root=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,11 @@ spec:
description: Phase represents the current phase (Ready, Reconciling,
Failed)
type: string
userNamespacesSupported:
description: |-
UserNamespacesSupported indicates if the cluster supports user namespaces in pods
(SCC userNamespaceLevel field). When false, workspace pods use privileged mode.
type: boolean
type: object
type: object
served: true
Expand Down
3 changes: 1 addition & 2 deletions config/rbac/pipeline_pod_rolebinding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ roleRef:
name: pipeline-pod-annotator
subjects:
- kind: ServiceAccount
name: pipeline
namespace: system
name: ado-build
3 changes: 1 addition & 2 deletions config/rbac/pipeline_registry_rolebinding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ roleRef:
name: registry-editor
subjects:
- kind: ServiceAccount
name: pipeline
namespace: system
name: ado-build
9 changes: 5 additions & 4 deletions internal/buildapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func createInternalRegistrySecret(ctx context.Context, restCfg *rest.Config, nam
},
}
tokenResp, err := clientset.CoreV1().ServiceAccounts(namespace).
CreateToken(ctx, "pipeline", tokenReq, metav1.CreateOptions{})
CreateToken(ctx, automotivev1alpha1.BuildServiceAccountName, tokenReq, metav1.CreateOptions{})
if err != nil {
return "", fmt.Errorf("error creating SA token: %w", err)
}
Expand Down Expand Up @@ -303,9 +303,9 @@ func (a *APIServer) mintRegistryToken(ctx context.Context, c *gin.Context, names
},
}
tokenResp, err := clientset.CoreV1().ServiceAccounts(namespace).
CreateToken(ctx, "pipeline", tokenReq, metav1.CreateOptions{})
CreateToken(ctx, automotivev1alpha1.BuildServiceAccountName, tokenReq, metav1.CreateOptions{})
if err != nil {
return "", metav1.Time{}, fmt.Errorf("error creating token for SA pipeline in %s: %w", namespace, err)
return "", metav1.Time{}, fmt.Errorf("error creating token for SA %s in %s: %w", automotivev1alpha1.BuildServiceAccountName, namespace, err)
}
return tokenResp.Status.Token, tokenResp.Status.ExpirationTimestamp, nil
}
Expand Down Expand Up @@ -3271,7 +3271,8 @@ func (a *APIServer) createFlash(c *gin.Context) {
},
},
Spec: tektonv1.TaskRunSpec{
TaskSpec: &flashTask.Spec,
ServiceAccountName: automotivev1alpha1.BuildServiceAccountName,
TaskSpec: &flashTask.Spec,
Params: []tektonv1.Param{
{Name: "image-ref", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: req.ImageRef}},
{Name: "exporter-selector", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: exporterSelector}},
Expand Down
2 changes: 1 addition & 1 deletion internal/common/tasks/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -1794,7 +1794,7 @@ func GenerateBuildBuilderJob(namespace, distro, targetRegistry, aibImage string)
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
ServiceAccountName: "pipeline",
ServiceAccountName: automotivev1alpha1.BuildServiceAccountName,
Containers: []corev1.Container{
{
Name: "build-helper",
Expand Down
2 changes: 2 additions & 0 deletions internal/controller/containerbuild/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ func (r *ContainerBuildReconciler) buildShipwrightBuildRun(
output.PushSecret = &pushSecret
}

saName := automotivev1alpha1.BuildServiceAccountName
buildRun := &shipwrightv1beta1.BuildRun{
ObjectMeta: metav1.ObjectMeta{
Name: buildRunName,
Expand All @@ -475,6 +476,7 @@ func (r *ContainerBuildReconciler) buildShipwrightBuildRun(
},
},
Spec: shipwrightv1beta1.BuildRunSpec{
ServiceAccount: &saName,
Build: shipwrightv1beta1.ReferencedBuild{
Spec: &shipwrightv1beta1.BuildSpec{
Source: &shipwrightv1beta1.Source{
Expand Down
5 changes: 3 additions & 2 deletions internal/controller/imagebuild/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ func (r *ImageBuildReconciler) createBuildTaskRun(
},
}
tokenResp, err := clientset.CoreV1().ServiceAccounts(imageBuild.Namespace).
CreateToken(ctx, "pipeline", tokenReq, metav1.CreateOptions{})
CreateToken(ctx, automotivev1alpha1.BuildServiceAccountName, tokenReq, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create SA token for flash OCI credentials: %w", err)
}
Expand Down Expand Up @@ -909,7 +909,8 @@ func (r *ImageBuildReconciler) createBuildTaskRun(
Params: params,
Workspaces: pipelineWorkspaces,
TaskRunTemplate: tektonv1.PipelineTaskRunTemplate{
PodTemplate: podTemplate,
PodTemplate: podTemplate,
ServiceAccountName: automotivev1alpha1.BuildServiceAccountName,
},
},
}
Expand Down
9 changes: 5 additions & 4 deletions internal/controller/imagereseal/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,13 @@ func (r *Reconciler) createSealedPipelineRun(ctx context.Context, sealed *automo
Tasks: pipelineTasks,
},
Workspaces: workspaces,
TaskRunTemplate: tektonv1.PipelineTaskRunTemplate{
ServiceAccountName: automotivev1alpha1.BuildServiceAccountName,
},
}
if nodeArch := archToNodeArch(sealed.Spec.Architecture); nodeArch != "" {
prSpec.TaskRunTemplate = tektonv1.PipelineTaskRunTemplate{
PodTemplate: &pod.Template{
NodeSelector: map[string]string{corev1.LabelArchStable: nodeArch},
},
prSpec.TaskRunTemplate.PodTemplate = &pod.Template{
NodeSelector: map[string]string{corev1.LabelArchStable: nodeArch},
}
}
pr := &tektonv1.PipelineRun{
Expand Down
64 changes: 46 additions & 18 deletions internal/controller/operatorconfig/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,15 +638,21 @@ func (r *OperatorConfigReconciler) deployOSBuilds(
return fmt.Errorf("failed to create/update pipeline: %w", err)
}

// On OpenShift, bind the pipeline SA to the privileged SCC for build pods
// Create the dedicated build SA (used by Tekton pods and token minting)
buildSA := r.buildBuildServiceAccount(config.Namespace)
if err := r.createOrUpdate(ctx, buildSA, config); err != nil {
return fmt.Errorf("failed to create/update build service account: %w", err)
}

// On OpenShift, bind the build SA to the privileged SCC for build pods
if r.detectOpenShift(ctx, config.Namespace) {
pipelineClusterRole := r.buildPipelineSCCClusterRole()
if err := r.createOrUpdate(ctx, pipelineClusterRole, config); err != nil {
return fmt.Errorf("failed to create/update pipeline SCC cluster role: %w", err)
buildClusterRole := r.buildBuildSCCClusterRole()
if err := r.createOrUpdate(ctx, buildClusterRole, config); err != nil {
return fmt.Errorf("failed to create/update build SCC cluster role: %w", err)
}
pipelineBinding := r.buildPipelineSCCRoleBinding(config.Namespace)
if err := r.createOrUpdate(ctx, pipelineBinding, config); err != nil {
return fmt.Errorf("failed to create/update pipeline SCC role binding: %w", err)
buildBinding := r.buildBuildSCCRoleBinding(config.Namespace)
if err := r.createOrUpdate(ctx, buildBinding, config); err != nil {
return fmt.Errorf("failed to create/update build SCC role binding: %w", err)
}
}

Expand Down Expand Up @@ -842,17 +848,23 @@ func (r *OperatorConfigReconciler) cleanupOSBuilds(ctx context.Context, config *
return fmt.Errorf("failed to cleanup build controller: %w", err)
}

// Cleanup pipeline SCC cluster role and binding
pipelineClusterRole := &rbacv1.ClusterRole{}
pipelineClusterRole.Name = sccPrivilegedRoleName
if err := r.Delete(ctx, pipelineClusterRole); err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete pipeline SCC cluster role: %w", err)
// Cleanup build SA and SCC bindings
buildSA := &corev1.ServiceAccount{}
buildSA.Name = automotivev1alpha1.BuildServiceAccountName
buildSA.Namespace = config.Namespace
if err := r.Delete(ctx, buildSA); err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete build service account: %w", err)
}
pipelineBinding := &rbacv1.RoleBinding{}
pipelineBinding.Name = pipelineSCCBindingName
pipelineBinding.Namespace = config.Namespace
if err := r.Delete(ctx, pipelineBinding); err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete pipeline SCC role binding: %w", err)
buildClusterRole := &rbacv1.ClusterRole{}
buildClusterRole.Name = sccPrivilegedRoleName
if err := r.Delete(ctx, buildClusterRole); err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete build SCC cluster role: %w", err)
}
buildBinding := &rbacv1.RoleBinding{}
buildBinding.Name = pipelineSCCBindingName
buildBinding.Namespace = config.Namespace
if err := r.Delete(ctx, buildBinding); err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete build SCC role binding: %w", err)
}

// Cleanup workspace infrastructure
Expand All @@ -873,13 +885,29 @@ func (r *OperatorConfigReconciler) deployWorkspaceInfra(ctx context.Context, con
return fmt.Errorf("failed to create/update workspace service account: %w", err)
}

// On OpenShift, create a custom SCC for workspace pods with user namespace support
// On OpenShift, create a custom SCC for workspace pods
if r.detectOpenShift(ctx, config.Namespace) {
scc := r.buildWorkspaceSCC()
if err := r.createOrUpdate(ctx, scc, config); err != nil {
return fmt.Errorf("failed to create/update workspace SCC: %w", err)
}

// Check if the cluster accepted userNamespaceLevel by reading the SCC back.
// OCP < 4.19 silently strips this field.
actual := &securityv1.SecurityContextConstraints{}
if err := r.Get(ctx, client.ObjectKey{Name: workspaceSCCName}, actual); err != nil {
return fmt.Errorf("failed to read back workspace SCC: %w", err)
}
config.Status.UserNamespacesSupported = actual.UserNamespaceLevel != ""
if !config.Status.UserNamespacesSupported {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
r.Log.Info("Cluster does not support user namespaces, workspace pods will use privileged mode")
// Re-create the SCC in privileged mode
scc = r.buildWorkspaceSCCPrivileged()
if err := r.createOrUpdate(ctx, scc, config); err != nil {
return fmt.Errorf("failed to create/update workspace SCC (privileged): %w", err)
}
}

clusterRole := r.buildWorkspaceSCCClusterRole()
if err := r.createOrUpdate(ctx, clusterRole, config); err != nil {
return fmt.Errorf("failed to create/update workspace SCC cluster role: %w", err)
Expand Down
77 changes: 66 additions & 11 deletions internal/controller/operatorconfig/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ import (

const (
buildControllerName = "ado-build-controller"
pipelineServiceAccountName = "pipeline"
pipelineSCCBindingName = "ado-pipeline-privileged"
sccPrivilegedRoleName = "ado-pipeline-privileged"
pipelineSCCBindingName = "ado-build-privileged"
sccPrivilegedRoleName = "ado-build-privileged"
workspaceServiceAccountName = "ado-workspace"
workspaceSCCName = "ado-workspace-scc"
)
Expand Down Expand Up @@ -906,35 +905,49 @@ func (r *OperatorConfigReconciler) buildWorkspaceSCCRoleBinding(namespace string
}
}

func (r *OperatorConfigReconciler) buildPipelineSCCClusterRole() *rbacv1.ClusterRole {
func (r *OperatorConfigReconciler) buildBuildServiceAccount(namespace string) *corev1.ServiceAccount {
return &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: automotivev1alpha1.BuildServiceAccountName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/name": "automotive-dev-operator",
"app.kubernetes.io/component": "build",
"app.kubernetes.io/part-of": "automotive-dev-operator",
},
},
}
}

func (r *OperatorConfigReconciler) buildBuildSCCClusterRole() *rbacv1.ClusterRole {
return &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: sccPrivilegedRoleName,
Labels: map[string]string{
"app.kubernetes.io/name": "automotive-dev-operator",
"app.kubernetes.io/component": "pipeline",
"app.kubernetes.io/component": "build",
"app.kubernetes.io/part-of": "automotive-dev-operator",
},
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{"security.openshift.io"},
Resources: []string{"securitycontextconstraints"},
ResourceNames: []string{"privileged"},
ResourceNames: []string{"privileged", "pipelines-scc"},
Verbs: []string{"use"},
},
},
}
}

func (r *OperatorConfigReconciler) buildPipelineSCCRoleBinding(namespace string) *rbacv1.RoleBinding {
func (r *OperatorConfigReconciler) buildBuildSCCRoleBinding(namespace string) *rbacv1.RoleBinding {
return &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: pipelineSCCBindingName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/name": "automotive-dev-operator",
"app.kubernetes.io/component": "pipeline",
"app.kubernetes.io/component": "build",
"app.kubernetes.io/part-of": "automotive-dev-operator",
},
},
Expand All @@ -946,7 +959,7 @@ func (r *OperatorConfigReconciler) buildPipelineSCCRoleBinding(namespace string)
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: pipelineServiceAccountName,
Name: automotivev1alpha1.BuildServiceAccountName,
Namespace: namespace,
},
},
Expand All @@ -969,8 +982,8 @@ func (r *OperatorConfigReconciler) buildWorkspaceSCC() *securityv1.SecurityConte
AllowHostPID: false,
AllowHostPorts: false,
AllowPrivilegeEscalation: ptr.To(true),
AllowPrivilegedContainer: true,
AllowedCapabilities: []corev1.Capability{"ALL"},
AllowPrivilegedContainer: false,
AllowedCapabilities: []corev1.Capability{"SETUID", "SETGID", "SYS_ADMIN", "DAC_OVERRIDE", "CHOWN", "FOWNER"},
FSGroup: securityv1.FSGroupStrategyOptions{
Type: securityv1.FSGroupStrategyMustRunAs,
Ranges: []securityv1.IDRange{{Min: 0, Max: 65534}},
Expand Down Expand Up @@ -1002,3 +1015,45 @@ func (r *OperatorConfigReconciler) buildWorkspaceSCC() *securityv1.SecurityConte
},
}
}

// buildWorkspaceSCCPrivileged creates a privileged SCC for clusters that don't
// support user namespaces (OCP < 4.19). This allows nested podman/buildah.
func (r *OperatorConfigReconciler) buildWorkspaceSCCPrivileged() *securityv1.SecurityContextConstraints {
return &securityv1.SecurityContextConstraints{
ObjectMeta: metav1.ObjectMeta{
Name: workspaceSCCName,
Labels: map[string]string{
"app.kubernetes.io/name": "automotive-dev-operator",
"app.kubernetes.io/component": "workspace",
"app.kubernetes.io/part-of": "automotive-dev-operator",
},
},
AllowHostDirVolumePlugin: false,
AllowHostIPC: false,
AllowHostNetwork: false,
AllowHostPID: false,
AllowHostPorts: false,
AllowPrivilegeEscalation: ptr.To(true),
AllowPrivilegedContainer: true,
AllowedCapabilities: []corev1.Capability{"*"},
FSGroup: securityv1.FSGroupStrategyOptions{
Type: securityv1.FSGroupStrategyRunAsAny,
},
RunAsUser: securityv1.RunAsUserStrategyOptions{
Type: securityv1.RunAsUserStrategyRunAsAny,
},
SELinuxContext: securityv1.SELinuxContextStrategyOptions{
Type: securityv1.SELinuxStrategyMustRunAs,
SELinuxOptions: &corev1.SELinuxOptions{
Type: "container_engine_t",
},
},
SeccompProfiles: []string{"*"},
SupplementalGroups: securityv1.SupplementalGroupsStrategyOptions{
Type: securityv1.SupplementalGroupsStrategyRunAsAny,
},
Volumes: []securityv1.FSType{
securityv1.FSTypeAll,
},
}
}
Loading
Loading