From 9d48c1852ffad7ebc4e07268a95b46452b98f12c Mon Sep 17 00:00:00 2001 From: Yun Pan Date: Wed, 12 Nov 2025 22:02:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(devbox):=20add=20startup=20configmap=20syn?= =?UTF-8?q?chronization=20and=20volume=20manage=E2=80=A6=20(#6175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(devbox): add startup configmap synchronization and volume management (#6172) * feat(devbox): add support for startup config map and related volume mounts * Update controllers/devbox/internal/controller/devbox_controller.go * Update controllers/devbox/internal/controller/devbox_controller.go * Update controllers/devbox/internal/controller/helper/devbox.go * Update controllers/devbox/internal/controller/helper/devbox.go * fix(devbox): ensure both DEVBOX_STARTUP_CM_NAME and DEVBOX_STARTUP_CM_NAMESPACE are set together * fix(devbox): update startup.sh check to ensure configmap data consistency * fix(devbox): ensure devboxConfigmap data is initialized (#877) --------- Signed-off-by: Yun Pan Co-authored-by: cuisongliu Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- controllers/devbox/cmd/main.go | 15 +++- controllers/devbox/config/rbac/role.yaml | 6 ++ .../devbox/deploy/manifests/deploy.yaml.tmpl | 6 ++ .../internal/controller/devbox_controller.go | 85 ++++++++++++++++++- .../internal/controller/helper/devbox.go | 26 ++++++ 5 files changed, 134 insertions(+), 4 deletions(-) diff --git a/controllers/devbox/cmd/main.go b/controllers/devbox/cmd/main.go index 01e234463..ca4631795 100644 --- a/controllers/devbox/cmd/main.go +++ b/controllers/devbox/cmd/main.go @@ -236,6 +236,13 @@ func main() { podMatchers = append(podMatchers, matcher.EphemeralStorageMatcher{}) } + startupCMName := os.Getenv("DEVBOX_STARTUP_CM_NAME") + startupCMNamespace := os.Getenv("DEVBOX_STARTUP_CM_NAMESPACE") + if (startupCMName != "" && startupCMNamespace == "") || (startupCMName == "" && startupCMNamespace != "") { + setupLog.Error(nil, "both DEVBOX_STARTUP_CM_NAME and DEVBOX_STARTUP_CM_NAMESPACE must be set together") + os.Exit(1) + } + if err = (&controller.DevboxReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -250,9 +257,11 @@ func main() { DefaultLimit: resource.MustParse(limitEphemeralStorage), MaximumLimit: resource.MustParse(maximumLimitEphemeralStorage), }, - PodMatchers: podMatchers, - DebugMode: debugMode, - RestartPredicateDuration: restartPredicateDuration, + PodMatchers: podMatchers, + DebugMode: debugMode, + StartupConfigMapName: startupCMName, + StartupConfigMapNamespace: startupCMNamespace, + RestartPredicateDuration: restartPredicateDuration, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Devbox") os.Exit(1) diff --git a/controllers/devbox/config/rbac/role.yaml b/controllers/devbox/config/rbac/role.yaml index c7916fa8c..70f91f320 100644 --- a/controllers/devbox/config/rbac/role.yaml +++ b/controllers/devbox/config/rbac/role.yaml @@ -18,6 +18,12 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - '*' - apiGroups: - "" resources: diff --git a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl index 84ad86671..01a6d8741 100644 --- a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -3379,6 +3379,12 @@ rules: - secrets verbs: - '*' +- apiGroups: + - "" + resources: + - configmaps + verbs: + - '*' - apiGroups: - "" resources: diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index 6e2e14d16..dd06a18cc 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -56,7 +56,9 @@ type DevboxReconciler struct { PodMatchers []matcher.PodMatcher - DebugMode bool + DebugMode bool + StartupConfigMapName string + StartupConfigMapNamespace string client.Client Scheme *runtime.Scheme @@ -73,6 +75,7 @@ type DevboxReconciler struct { // +kubebuilder:rbac:groups="",resources=pods/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=services,verbs=* // +kubebuilder:rbac:groups="",resources=secrets,verbs=* +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=* // +kubebuilder:rbac:groups="",resources=events,verbs=* func (r *DevboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -89,6 +92,10 @@ func (r *DevboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr PartOf: devboxv1alpha1.DevBoxPartOf, }) + logger.Info("start reconciling devbox", "devbox", devbox.Name) + if r.StartupConfigMapName != "" { + logger.Info("startup config map set", "startupConfigMapName", r.StartupConfigMapName, "startupConfigMapNamespace", r.StartupConfigMapNamespace) + } if devbox.ObjectMeta.DeletionTimestamp.IsZero() { // retry add finalizer err := retry.RetryOnConflict(retry.DefaultRetry, func() error { @@ -132,6 +139,18 @@ func (r *DevboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr logger.Info("sync secret success") r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync secret success", "Sync secret success") + if r.StartupConfigMapName != "" { + // create or update startup configmap + logger.Info("syncing startup configmap") + if err := r.syncStartupConfigMap(ctx, devbox, recLabels); err != nil { + logger.Error(err, "sync startup configmap failed") + r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Sync startup configmap failed", "%v", err) + return ctrl.Result{}, err + } + logger.Info("sync startup configmap success") + r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync startup configmap success", "Sync startup configmap success") + } + // create service if network type is NodePort if devbox.Spec.NetworkSpec.Type == devboxv1alpha1.NetworkTypeNodePort { logger.Info("syncing service") @@ -161,6 +180,60 @@ func (r *DevboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, nil } +func (r *DevboxReconciler) syncStartupConfigMap(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error { + objectMeta := metav1.ObjectMeta{ + Name: devbox.Name, + Namespace: devbox.Namespace, + Labels: recLabels, + } + devboxConfigmap := &corev1.ConfigMap{ + ObjectMeta: objectMeta, + } + + startupConfigMap := &corev1.ConfigMap{} + err := r.Get(ctx, client.ObjectKey{Namespace: r.StartupConfigMapNamespace, Name: r.StartupConfigMapName}, startupConfigMap) + if err != nil { + return fmt.Errorf("failed to get startup configmap: %w", err) + } + if startupConfigMap.Data == nil || startupConfigMap.Data["startup.sh"] == "" { + return fmt.Errorf("startup configmap %s/%s is missing the 'startup.sh' key or it is empty", r.StartupConfigMapNamespace, r.StartupConfigMapName) + } + err = r.Get(ctx, client.ObjectKey{Namespace: devbox.Namespace, Name: devbox.Name}, devboxConfigmap) + if err == nil { + // configmap already exists, no need to create + if devboxConfigmap.Data == nil { + devboxConfigmap.Data = make(map[string]string) + } + if _, ok := devboxConfigmap.Data["startup.sh"]; !ok || devboxConfigmap.Data["startup.sh"] != startupConfigMap.Data["startup.sh"] { + devboxConfigmap.Data["startup.sh"] = startupConfigMap.Data["startup.sh"] + if err := r.Update(ctx, devboxConfigmap); err != nil { + return fmt.Errorf("failed to update configmap: %w", err) + } + } + + return nil + } + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to get configmap: %w", err) + } + + configmap := &corev1.ConfigMap{ + ObjectMeta: objectMeta, + Data: map[string]string{ + "startup.sh": startupConfigMap.Data["startup.sh"], + }, + } + + if err := controllerutil.SetControllerReference(devbox, configmap, r.Scheme); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + + if err := r.Create(ctx, configmap); err != nil { + return fmt.Errorf("failed to create configmap: %w", err) + } + return nil +} + func (r *DevboxReconciler) syncSecret(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error { objectMeta := metav1.ObjectMeta{ Name: devbox.Name, @@ -522,6 +595,10 @@ func (r *DevboxReconciler) removeAll(ctx context.Context, devbox *devboxv1alpha1 if err := r.deleteResourcesByLabels(ctx, &corev1.Service{}, devbox.Namespace, recLabels); err != nil { return err } + // Delete Configmap + if err := r.deleteResourcesByLabels(ctx, &corev1.ConfigMap{}, devbox.Namespace, recLabels); err != nil { + return err + } // Delete Secret return r.deleteResourcesByLabels(ctx, &corev1.Secret{}, devbox.Namespace, recLabels) } @@ -559,9 +636,15 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, next volumes := devbox.Spec.Config.Volumes volumes = append(volumes, helper.GenerateSSHVolume(devbox)) + if r.StartupConfigMapName != "" { + volumes = append(volumes, helper.GenerateStartupVolume(devbox)) + } volumeMounts := devbox.Spec.Config.VolumeMounts volumeMounts = append(volumeMounts, helper.GenerateSSHVolumeMounts()...) + if r.StartupConfigMapName != "" { + volumeMounts = append(volumeMounts, helper.GenerateStartupVolumeMounts()...) + } containers := []corev1.Container{ { diff --git a/controllers/devbox/internal/controller/helper/devbox.go b/controllers/devbox/internal/controller/helper/devbox.go index d73b0a9e7..d5f6bde5f 100644 --- a/controllers/devbox/internal/controller/helper/devbox.go +++ b/controllers/devbox/internal/controller/helper/devbox.go @@ -326,6 +326,32 @@ func GenerateSSHVolume(devbox *devboxv1alpha1.Devbox) corev1.Volume { } } +// GenerateStartupVolume generates a volume for the startup script configmap +func GenerateStartupVolume(devbox *devboxv1alpha1.Devbox) corev1.Volume { + return corev1.Volume{ + Name: "devbox-startup", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: devbox.Name, + }, + DefaultMode: ptr.To(int32(0755)), + }, + }, + } +} +// GenerateStartupVolumeMounts generates volume mounts for the startup script +func GenerateStartupVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + { + Name: "devbox-startup", + MountPath: "/usr/start/startup.sh", + SubPath: "startup.sh", + ReadOnly: true, + }, + } +} + // GenerateResourceRequirements generates the resource requirements for the Devbox pod func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, requestRate utilsresource.RequestRate, ephemeralStorage utilsresource.EphemeralStorage) corev1.ResourceRequirements { return corev1.ResourceRequirements{