Skip to content

Commit 6af5fb8

Browse files
committed
Support internal OCP registry
The internal OCP registry is supported by default without a need of providing any registry auth secret. The backup image is pushed to the same namespace where the workspace is running. The token is auto-generated and mounted from the SA definition. Signed-off-by: Ales Raszka <[email protected]>
1 parent 9d2b116 commit 6af5fb8

15 files changed

+463
-52
lines changed

apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ type CleanupCronJobConfig struct {
7474

7575
type RegistryConfig struct {
7676
// A registry where backup images are stored. Images are stored
77-
// in {path}/${DEVWORKSPACE_NAMESPACE}:${DEVWORKSPACE_NAME}
77+
// in {path}/${DEVWORKSPACE_NAMESPACE}/${DEVWORKSPACE_NAME}:latest
7878
// +kubebuilder:validation:Required
7979
Path string `json:"path,omitempty"`
8080
// AuthSecret is the name of a Kubernetes secret of

controllers/backupcronjob/backupcronjob_controller.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ func (r *BackupCronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
122122
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;create;update;patch;delete
123123
// +kubebuilder:rbac:groups=controller.devfile.io,resources=devworkspaceoperatorconfigs,verbs=get;list;update;patch;watch
124124
// +kubebuilder:rbac:groups=workspace.devfile.io,resources=devworkspaces,verbs=get;list
125+
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;create;update;patch;delete
126+
// +kubebuilder:rbac:groups="",resources=builds,verbs=get
127+
// +kubebuilder:rbac:groups="",resources=builds/details,verbs=update
128+
// +kubebuilder:rbac:groups="",resources=imagestreams,verbs=create
129+
// +kubebuilder:rbac:groups="",resources=imagestreams/layers,verbs=get;update
130+
// +kubebuilder:rbac:groups=build.openshift.io,resources=builds,verbs=get
131+
// +kubebuilder:rbac:groups=build.openshift.io,resources=builds/details,verbs=update
132+
// +kubebuilder:rbac:groups=image.openshift.io,resources=imagestreams,verbs=get;list;create;update;patch;delete
133+
// +kubebuilder:rbac:groups=image.openshift.io,resources=imagestreams/layers,verbs=get;update
125134

126135
// Reconcile is the main reconciliation loop for the BackupCronJob controller.
127136
func (r *BackupCronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -456,11 +465,11 @@ func (r *BackupCronJobReconciler) getWorkspacePVCName(ctx context.Context, works
456465
func (r *BackupCronJobReconciler) handleRegistryAuthSecret(ctx context.Context, workspace *dw.DevWorkspace,
457466
dwOperatorConfig *controllerv1alpha1.DevWorkspaceOperatorConfig, log logr.Logger,
458467
) (*corev1.Secret, error) {
459-
if dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret == "" {
468+
secretName := dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret
469+
if secretName == "" {
460470
// No auth secret configured - anonymous access to registry
461471
return nil, nil
462472
}
463-
secretName := dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret
464473

465474
// First check the workspace namespace for the secret
466475
registryAuthSecret := &corev1.Secret{}
@@ -478,21 +487,18 @@ func (r *BackupCronJobReconciler) handleRegistryAuthSecret(ctx context.Context,
478487
log.Info("Registry auth secret not found in workspace namespace, checking operator namespace", "secretName", secretName)
479488

480489
// If the secret is not found in the workspace namespace, check the operator namespace as fallback
481-
if dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret != "" {
482-
err := r.NonCachingClient.Get(ctx, client.ObjectKey{
483-
Name: dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret,
484-
Namespace: dwOperatorConfig.Namespace,
485-
}, registryAuthSecret)
486-
if err != nil {
487-
log.Error(err, "Failed to get registry auth secret for backup job", "secretName", dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret)
488-
return nil, err
489-
}
490-
log.Info("Successfully retrieved registry auth secret for backup job", "secretName", dwOperatorConfig.Config.Workspace.BackupCronJob.Registry.AuthSecret)
491-
return r.copySecret(ctx, workspace, registryAuthSecret, log)
490+
err = r.NonCachingClient.Get(ctx, client.ObjectKey{
491+
Name: secretName,
492+
Namespace: dwOperatorConfig.Namespace}, registryAuthSecret)
493+
if err != nil {
494+
log.Error(err, "Failed to get registry auth secret for backup job", "secretName", secretName)
495+
return nil, err
492496
}
493-
return nil, nil
497+
log.Info("Successfully retrieved registry auth secret for backup job", "secretName", secretName)
498+
return r.copySecret(ctx, workspace, registryAuthSecret, log)
494499
}
495500

501+
// copySecret copies the given secret from the operator namespace to the workspace namespace.
496502
func (r *BackupCronJobReconciler) copySecret(ctx context.Context, workspace *dw.DevWorkspace, sourceSecret *corev1.Secret, log logr.Logger) (namespaceSecret *corev1.Secret, err error) {
497503
existingNamespaceSecret := &corev1.Secret{}
498504
err = r.NonCachingClient.Get(ctx, client.ObjectKey{

controllers/backupcronjob/backupcronjob_controller_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
4040
"github.com/devfile/devworkspace-operator/pkg/conditions"
4141
"github.com/devfile/devworkspace-operator/pkg/constants"
42+
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
4243
)
4344

4445
var _ = Describe("BackupCronJobReconciler", func() {
@@ -52,6 +53,10 @@ var _ = Describe("BackupCronJobReconciler", func() {
5253

5354
BeforeEach(func() {
5455
ctx = context.Background()
56+
57+
// Initialize infrastructure for testing (defaults to Kubernetes)
58+
infrastructure.InitializeForTesting(infrastructure.Kubernetes)
59+
5560
scheme := runtime.NewScheme()
5661
Expect(controllerv1alpha1.AddToScheme(scheme)).To(Succeed())
5762
Expect(dwv2.AddToScheme(scheme)).To(Succeed())

controllers/backupcronjob/rbac.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ import (
2121

2222
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2323
"github.com/devfile/devworkspace-operator/pkg/constants"
24+
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
2425
corev1 "k8s.io/api/core/v1"
26+
rbacv1 "k8s.io/api/rbac/v1"
2527
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
29+
"k8s.io/apimachinery/pkg/runtime/schema"
2630
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2731
)
2832

@@ -31,8 +35,9 @@ const (
3135
)
3236

3337
func (r *BackupCronJobReconciler) ensureJobRunnerRBAC(ctx context.Context, workspace *dw.DevWorkspace) error {
38+
saName := JobRunnerSAName + "-" + workspace.Status.DevWorkspaceId
3439
sa := &corev1.ServiceAccount{
35-
ObjectMeta: metav1.ObjectMeta{Name: JobRunnerSAName + "-" + workspace.Status.DevWorkspaceId, Namespace: workspace.Namespace, Labels: map[string]string{
40+
ObjectMeta: metav1.ObjectMeta{Name: saName, Namespace: workspace.Namespace, Labels: map[string]string{
3641
constants.DevWorkspaceIDLabel: workspace.Status.DevWorkspaceId,
3742
constants.DevWorkspaceWatchSecretLabel: "true",
3843
}},
@@ -47,6 +52,86 @@ func (r *BackupCronJobReconciler) ensureJobRunnerRBAC(ctx context.Context, works
4752
return fmt.Errorf("ensuring ServiceAccount: %w", err)
4853
}
4954

55+
if infrastructure.IsOpenShift() {
56+
// Create ClusterRoleBinding for image push role
57+
if err := r.ensureImagePushRoleBinding(ctx, saName, workspace); err != nil {
58+
return fmt.Errorf("ensuring image push ClusterRoleBinding: %w", err)
59+
}
60+
// Create ImageStream for backup images
61+
if err := r.ensureImageStreamForBackup(ctx, workspace); err != nil {
62+
return fmt.Errorf("ensuring ImageStream for backup: %w", err)
63+
}
64+
}
65+
5066
return nil
5167

5268
}
69+
70+
// ensureImagePushRoleBinding creates a ClusterRoleBinding to allow the given ServiceAccount to push images
71+
// to the OpenShift internal registry.
72+
func (r *BackupCronJobReconciler) ensureImagePushRoleBinding(ctx context.Context, saName string, workspace *dw.DevWorkspace) error {
73+
// Create ClusterRoleBinding for system:image-builder role
74+
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
75+
ObjectMeta: metav1.ObjectMeta{
76+
Name: "devworkspace-image-builder-" + workspace.Status.DevWorkspaceId,
77+
Labels: map[string]string{
78+
constants.DevWorkspaceIDLabel: workspace.Status.DevWorkspaceId,
79+
},
80+
},
81+
Subjects: []rbacv1.Subject{
82+
{
83+
Kind: rbacv1.ServiceAccountKind,
84+
Name: saName,
85+
Namespace: workspace.Namespace,
86+
},
87+
},
88+
RoleRef: rbacv1.RoleRef{
89+
Kind: "ClusterRole",
90+
Name: "system:image-builder",
91+
APIGroup: "rbac.authorization.k8s.io",
92+
},
93+
}
94+
95+
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, clusterRoleBinding, func() error { return nil }); err != nil {
96+
return fmt.Errorf("ensuring ClusterRoleBinding: %w", err)
97+
}
98+
return nil
99+
}
100+
101+
// ensureImageStreamForBackup creates an ImageStream for the backup images in OpenShift in case user
102+
// selects to use the internal registry. Push to non-existing ImageStream fails, so we need to create it first.
103+
func (r *BackupCronJobReconciler) ensureImageStreamForBackup(ctx context.Context, workspace *dw.DevWorkspace) error {
104+
// Create ImageStream for backup images using unstructured to avoid scheme conflicts
105+
imageStream := &unstructured.Unstructured{
106+
Object: map[string]interface{}{
107+
"apiVersion": "image.openshift.io/v1",
108+
"kind": "ImageStream",
109+
"metadata": map[string]interface{}{
110+
"name": workspace.Name,
111+
"namespace": workspace.Namespace,
112+
"labels": map[string]interface{}{
113+
constants.DevWorkspaceIDLabel: workspace.Status.DevWorkspaceId,
114+
},
115+
},
116+
"spec": map[string]interface{}{
117+
"lookupPolicy": map[string]interface{}{
118+
"local": true,
119+
},
120+
},
121+
},
122+
}
123+
imageStream.SetGroupVersionKind(schema.GroupVersionKind{
124+
Group: "image.openshift.io",
125+
Version: "v1",
126+
Kind: "ImageStream",
127+
})
128+
129+
if err := controllerutil.SetControllerReference(workspace, imageStream, r.Scheme); err != nil {
130+
return err
131+
}
132+
133+
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, imageStream, func() error { return nil }); err != nil {
134+
return fmt.Errorf("ensuring ImageStream: %w", err)
135+
}
136+
return nil
137+
}

deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deploy/bundle/manifests/devworkspace-operator.clusterserviceversion.yaml

Lines changed: 49 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)