feat(devbox): add startup configmap synchronization and volume manage… (#6175)

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 <dinoallo@netc.it>
Co-authored-by: cuisongliu <cuisongliu@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Yun Pan
2025-11-12 22:02:27 +08:00
committed by GitHub
co-authored by cuisongliu Copilot
parent fff9215f64
commit 9d48c1852f
5 changed files with 134 additions and 4 deletions
+12 -3
View File
@@ -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)
+6
View File
@@ -18,6 +18,12 @@ kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- '*'
- apiGroups:
- ""
resources:
@@ -3379,6 +3379,12 @@ rules:
- secrets
verbs:
- '*'
- apiGroups:
- ""
resources:
- configmaps
verbs:
- '*'
- apiGroups:
- ""
resources:
@@ -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{
{
@@ -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{