Files
sealos/controllers/devbox/internal/controller/devbox_controller.go
T
9d48c1852f 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>
2025-11-12 22:02:27 +08:00

743 lines
26 KiB
Go

/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"fmt"
"time"
devboxv1alpha1 "github.com/labring/sealos/controllers/devbox/api/v1alpha1"
"github.com/labring/sealos/controllers/devbox/internal/controller/helper"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/matcher"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource"
"github.com/labring/sealos/controllers/devbox/label"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
// DevboxReconciler reconciles a Devbox object
type DevboxReconciler struct {
CommitImageRegistry string
RequestRate resource.RequestRate
EphemeralStorage resource.EphemeralStorage
PodMatchers []matcher.PodMatcher
DebugMode bool
StartupConfigMapName string
StartupConfigMapNamespace string
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
RestartPredicateDuration time.Duration
}
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxes/finalizers,verbs=update
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=runtimes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=runtimeclasses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=pods,verbs=*
// +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) {
logger := log.FromContext(ctx)
devbox := &devboxv1alpha1.Devbox{}
if err := r.Get(ctx, req.NamespacedName, devbox); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
recLabels := label.RecommendedLabels(&label.Recommended{
Name: devbox.Name,
ManagedBy: label.DefaultManagedBy,
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 {
latestDevbox := &devboxv1alpha1.Devbox{}
if err := r.Get(ctx, req.NamespacedName, latestDevbox); err != nil {
return client.IgnoreNotFound(err)
}
if controllerutil.AddFinalizer(latestDevbox, devboxv1alpha1.FinalizerName) {
return r.Update(ctx, latestDevbox)
}
return nil
})
if err != nil {
return ctrl.Result{}, err
}
} else {
logger.Info("devbox deleted, remove all resources")
if err := r.removeAll(ctx, devbox, recLabels); err != nil {
return ctrl.Result{}, err
}
logger.Info("devbox deleted, remove finalizer")
if controllerutil.RemoveFinalizer(devbox, devboxv1alpha1.FinalizerName) {
if err := r.Update(ctx, devbox); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
devbox.Status.Network.Type = devbox.Spec.NetworkSpec.Type
_ = r.Status().Update(ctx, devbox)
// create or update secret
logger.Info("syncing secret")
if err := r.syncSecret(ctx, devbox, recLabels); err != nil {
logger.Error(err, "sync secret failed")
r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Sync secret failed", "%v", err)
return ctrl.Result{}, err
}
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")
if err := r.Get(ctx, req.NamespacedName, devbox); err != nil {
return ctrl.Result{}, err
}
if err := r.syncService(ctx, devbox, recLabels); err != nil {
logger.Error(err, "sync service failed")
r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Sync service failed", "%v", err)
return ctrl.Result{}, err
}
logger.Info("sync service success")
r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync service success", "Sync service success")
}
// create or update pod
logger.Info("syncing pod")
if err := r.syncPod(ctx, devbox, recLabels); err != nil {
logger.Error(err, "sync pod failed")
r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Sync pod failed", "%v", err)
return ctrl.Result{}, err
}
logger.Info("sync pod success")
r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync pod success", "Sync pod success")
logger.Info("devbox reconcile success")
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,
Namespace: devbox.Namespace,
Labels: recLabels,
}
devboxSecret := &corev1.Secret{
ObjectMeta: objectMeta,
}
err := r.Get(ctx, client.ObjectKey{Namespace: devbox.Namespace, Name: devbox.Name}, devboxSecret)
if err == nil {
// Secret already exists, no need to create
// TODO: delete this code after we have a way to sync secret to devbox
// check if SEALOS_DEVBOX_JWT_SECRET is exist, if not exist, create it
if _, ok := devboxSecret.Data["SEALOS_DEVBOX_JWT_SECRET"]; !ok {
devboxSecret.Data["SEALOS_DEVBOX_JWT_SECRET"] = []byte(rand.String(32))
if err := r.Update(ctx, devboxSecret); err != nil {
return fmt.Errorf("failed to update secret: %w", err)
}
}
if _, ok := devboxSecret.Data["SEALOS_DEVBOX_AUTHORIZED_KEYS"]; !ok {
devboxSecret.Data["SEALOS_DEVBOX_AUTHORIZED_KEYS"] = devboxSecret.Data["SEALOS_DEVBOX_PUBLIC_KEY"]
if err := r.Update(ctx, devboxSecret); err != nil {
return fmt.Errorf("failed to update secret: %w", err)
}
}
return nil
}
if client.IgnoreNotFound(err) != nil {
return fmt.Errorf("failed to get secret: %w", err)
}
// Secret not found, create a new one
publicKey, privateKey, err := helper.GenerateSSHKeyPair()
if err != nil {
return fmt.Errorf("failed to generate SSH key pair: %w", err)
}
secret := &corev1.Secret{
ObjectMeta: objectMeta,
Data: map[string][]byte{
"SEALOS_DEVBOX_JWT_SECRET": []byte(rand.String(32)),
"SEALOS_DEVBOX_PUBLIC_KEY": publicKey,
"SEALOS_DEVBOX_PRIVATE_KEY": privateKey,
"SEALOS_DEVBOX_AUTHORIZED_KEYS": publicKey,
},
}
if err := controllerutil.SetControllerReference(devbox, secret, r.Scheme); err != nil {
return fmt.Errorf("failed to set controller reference: %w", err)
}
if err := r.Create(ctx, secret); err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}
return nil
}
func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error {
logger := log.FromContext(ctx)
var podList corev1.PodList
if err := r.List(ctx, &podList, client.InNamespace(devbox.Namespace), client.MatchingLabels(recLabels)); err != nil {
return err
}
// only one pod is allowed, if more than one pod found, return error
if len(podList.Items) > 1 {
// remove finalizer and delete them
for _, pod := range podList.Items {
if controllerutil.RemoveFinalizer(&pod, devboxv1alpha1.FinalizerName) {
if err := r.Update(ctx, &pod); err != nil {
logger.Error(err, "remove finalizer failed")
}
}
if err := r.Delete(ctx, &pod); err != nil {
logger.Error(err, "delete pod failed")
}
}
return fmt.Errorf("more than one pod found")
}
logger.Info("pod list", "length", len(podList.Items))
// update devbox status after pod is created or updated
defer func() {
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
logger.Info("update devbox status after pod synced")
latestDevbox := &devboxv1alpha1.Devbox{}
if err := r.Client.Get(ctx, client.ObjectKey{Namespace: devbox.Namespace, Name: devbox.Name}, latestDevbox); err != nil {
logger.Error(err, "get latest devbox failed")
return err
}
// update devbox status with latestDevbox status
logger.Info("updating devbox status")
logger.Info("merge commit history", "devbox", devbox.Status.CommitHistory, "latestDevbox", latestDevbox.Status.CommitHistory)
devbox.Status.Phase = helper.GenerateDevboxPhase(devbox, podList)
helper.UpdateDevboxStatus(devbox, latestDevbox)
return r.Status().Update(ctx, latestDevbox)
}); err != nil {
logger.Error(err, "sync pod failed")
r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Sync pod failed", "%v", err)
return
}
logger.Info("update devbox status success")
r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync pod success", "Sync pod success")
}()
switch devbox.Spec.State {
case devboxv1alpha1.DevboxStateRunning:
nextCommitHistory := r.generateNextCommitHistory(devbox)
expectPod := r.generateDevboxPod(devbox, nextCommitHistory)
switch len(podList.Items) {
case 0:
logger.Info("create pod")
logger.Info("next commit history", "commit", nextCommitHistory)
err := r.createPod(ctx, devbox, expectPod, nextCommitHistory)
if err != nil && helper.IsExceededQuotaError(err) {
logger.Info("devbox is exceeded quota, change devbox state to Stopped")
r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Devbox is exceeded quota", "Devbox is exceeded quota")
devbox.Spec.State = devboxv1alpha1.DevboxStateStopped
_ = r.Update(ctx, devbox)
return nil
}
if err != nil {
logger.Error(err, "create pod failed")
return err
}
return nil
case 1:
pod := &podList.Items[0]
// check pod container size, if it is 0, it means the pod is not running, return an error
if len(pod.Status.ContainerStatuses) == 0 {
return fmt.Errorf("pod container size is 0")
}
devbox.Status.State = pod.Status.ContainerStatuses[0].State
// update commit predicated status by pod status, this should be done once find a pod
helper.UpdatePredicatedCommitStatus(devbox, pod)
// pod has been deleted, handle it, next reconcile will create a new pod, and we will update commit history status by predicated status
if !pod.DeletionTimestamp.IsZero() {
logger.Info("pod has been deleted")
return r.handlePodDeleted(ctx, devbox, pod)
}
switch matcher.PodMatchExpectations(expectPod, pod, r.PodMatchers...) {
case true:
// pod match expectations
logger.Info("pod match expectations")
switch pod.Status.Phase {
case corev1.PodPending, corev1.PodRunning:
// pod is running or pending, do nothing here
logger.Info("pod is running or pending")
// update commit history status by pod status
helper.UpdateCommitHistory(devbox, pod, false)
return nil
case corev1.PodFailed, corev1.PodSucceeded:
// pod failed or succeeded, we need delete pod and remove finalizer
logger.Info("pod failed or succeeded, recreate pod")
return r.deletePod(ctx, devbox, pod)
}
case false:
// pod not match expectations, delete pod anyway
logger.Info("pod not match expectations, recreate pod")
return r.deletePod(ctx, devbox, pod)
}
}
case devboxv1alpha1.DevboxStateStopped, devboxv1alpha1.DevboxStateShutdown:
switch len(podList.Items) {
case 0:
return nil
case 1:
pod := &podList.Items[0]
// update state to empty since devbox is stopped
devbox.Status.State = corev1.ContainerState{}
// update commit predicated status by pod status, this should be done once find a pod
helper.UpdatePredicatedCommitStatus(devbox, pod)
// pod has been deleted, handle it, next reconcile will create a new pod, and we will update commit history status by predicated status
if !pod.DeletionTimestamp.IsZero() {
return r.handlePodDeleted(ctx, devbox, pod)
}
// we need delete pod because devbox state is stopped
// we don't care about the pod status, just delete it
return r.deletePod(ctx, devbox, pod)
}
}
return nil
}
func (r *DevboxReconciler) syncService(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error {
var servicePorts []corev1.ServicePort
for _, port := range devbox.Spec.Config.Ports {
servicePorts = append(servicePorts, corev1.ServicePort{
Name: port.Name,
Port: port.ContainerPort,
TargetPort: intstr.FromInt32(port.ContainerPort),
Protocol: port.Protocol,
})
}
if len(servicePorts) == 0 {
//use the default value
servicePorts = []corev1.ServicePort{
{
Name: "devbox-ssh-port",
Port: 22,
TargetPort: intstr.FromInt32(22),
Protocol: corev1.ProtocolTCP,
},
}
}
expectServiceSpec := corev1.ServiceSpec{
Selector: recLabels,
Type: corev1.ServiceTypeNodePort,
Ports: servicePorts,
}
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: devbox.Name + "-svc",
Namespace: devbox.Namespace,
Labels: recLabels,
},
}
switch devbox.Spec.State {
case devboxv1alpha1.DevboxStateShutdown:
err := r.Client.Delete(ctx, service)
if err != nil && !errors.IsNotFound(err) {
return err
}
devbox.Status.Network = devboxv1alpha1.NetworkStatus{
Type: devboxv1alpha1.NetworkTypeNodePort,
NodePort: int32(0),
}
return r.Status().Update(ctx, devbox)
case devboxv1alpha1.DevboxStateRunning, devboxv1alpha1.DevboxStateStopped:
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, service, func() error {
// only update some specific fields
service.Spec.Selector = expectServiceSpec.Selector
service.Spec.Type = expectServiceSpec.Type
if len(service.Spec.Ports) == 0 {
service.Spec.Ports = expectServiceSpec.Ports
} else {
service.Spec.Ports[0].Name = expectServiceSpec.Ports[0].Name
service.Spec.Ports[0].Port = expectServiceSpec.Ports[0].Port
service.Spec.Ports[0].TargetPort = expectServiceSpec.Ports[0].TargetPort
service.Spec.Ports[0].Protocol = expectServiceSpec.Ports[0].Protocol
}
return controllerutil.SetControllerReference(devbox, service, r.Scheme)
}); err != nil {
return err
}
// Retrieve the updated Service to get the NodePort
var updatedService corev1.Service
err := retry.OnError(
retry.DefaultRetry,
func(err error) bool { return client.IgnoreNotFound(err) == nil },
func() error {
return r.Client.Get(ctx, client.ObjectKey{Namespace: service.Namespace, Name: service.Name}, &updatedService)
})
if err != nil {
return fmt.Errorf("failed to get updated service: %w", err)
}
// Extract the NodePort
nodePort := int32(0)
for _, port := range updatedService.Spec.Ports {
if port.NodePort != 0 {
nodePort = port.NodePort
break
}
}
if nodePort == 0 {
return fmt.Errorf("NodePort not found for service %s", service.Name)
}
devbox.Status.Network.Type = devboxv1alpha1.NetworkTypeNodePort
devbox.Status.Network.NodePort = nodePort
return r.Status().Update(ctx, devbox)
}
return nil
}
// create a new pod, add predicated status to nextCommitHistory
func (r *DevboxReconciler) createPod(ctx context.Context, devbox *devboxv1alpha1.Devbox, expectPod *corev1.Pod, nextCommitHistory *devboxv1alpha1.CommitHistory) error {
logger := log.FromContext(ctx)
logger.Info("creating pod",
"podName", expectPod.Name,
"namespace", expectPod.Namespace,
"nextCommitHistory", nextCommitHistory)
nextCommitHistory.Status = devboxv1alpha1.CommitStatusPending
nextCommitHistory.PredicatedStatus = devboxv1alpha1.CommitStatusPending
if expectPod.Name == "" {
return fmt.Errorf("pod name cannot be empty")
}
if err := r.Create(ctx, expectPod); err != nil {
logger.Error(err, "failed to create pod")
return err
}
devbox.Status.CommitHistory = append(devbox.Status.CommitHistory, nextCommitHistory)
return nil
}
func (r *DevboxReconciler) deletePod(ctx context.Context, devbox *devboxv1alpha1.Devbox, pod *corev1.Pod) error {
logger := log.FromContext(ctx)
// remove finalizer and delete pod
controllerutil.RemoveFinalizer(pod, devboxv1alpha1.FinalizerName)
if err := r.Update(ctx, pod); err != nil {
logger.Error(err, "remove finalizer failed")
return err
}
if err := r.Delete(ctx, pod, client.GracePeriodSeconds(0), client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil {
logger.Error(err, "delete pod failed")
return err
}
// update commit history status because pod has been deleted
if len(pod.Status.ContainerStatuses) != 0 {
devbox.Status.LastTerminationState = pod.Status.ContainerStatuses[0].State
}
helper.UpdateCommitHistory(devbox, pod, true)
return nil
}
func (r *DevboxReconciler) handlePodDeleted(ctx context.Context, devbox *devboxv1alpha1.Devbox, pod *corev1.Pod) error {
logger := log.FromContext(ctx)
controllerutil.RemoveFinalizer(pod, devboxv1alpha1.FinalizerName)
if err := r.Update(ctx, pod); err != nil {
logger.Error(err, "remove finalizer failed")
return err
}
// update commit history status because pod has been deleted
if len(pod.Status.ContainerStatuses) != 0 {
devbox.Status.LastTerminationState = pod.Status.ContainerStatuses[0].State
}
helper.UpdateCommitHistory(devbox, pod, true)
return nil
}
func (r *DevboxReconciler) removeAll(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error {
// Delete Pod
podList := &corev1.PodList{}
if err := r.List(ctx, podList, client.InNamespace(devbox.Namespace), client.MatchingLabels(recLabels)); err != nil {
return err
}
for _, pod := range podList.Items {
if controllerutil.RemoveFinalizer(&pod, devboxv1alpha1.FinalizerName) {
if err := r.Update(ctx, &pod); err != nil {
return err
}
}
}
if err := r.deleteResourcesByLabels(ctx, &corev1.Pod{}, devbox.Namespace, recLabels); err != nil {
return err
}
// Delete Service
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)
}
func (r *DevboxReconciler) deleteResourcesByLabels(ctx context.Context, obj client.Object, namespace string, labels map[string]string) error {
err := r.DeleteAllOf(ctx, obj,
client.InNamespace(namespace),
client.MatchingLabels(labels),
)
return client.IgnoreNotFound(err)
}
func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, nextCommitHistory *devboxv1alpha1.CommitHistory) *corev1.Pod {
objectMeta := metav1.ObjectMeta{
Name: nextCommitHistory.Pod,
Namespace: devbox.Namespace,
Labels: helper.GeneratePodLabels(devbox),
Annotations: helper.GeneratePodAnnotations(devbox),
}
ports := devbox.Spec.Config.Ports
// TODO: add extra ports to pod, currently not support
// ports = append(ports, devbox.Spec.NetworkSpec.ExtraPorts...)
envs := devbox.Spec.Config.Env
envs = append(envs, helper.GenerateDevboxEnvVars(devbox, nextCommitHistory)...)
//get image name
var imageName string
if r.DebugMode {
imageName = devbox.Spec.Image
} else {
imageName = helper.GetLastSuccessCommitImageName(devbox)
}
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{
{
Name: devbox.ObjectMeta.Name,
Image: imageName,
Env: envs,
Ports: ports,
VolumeMounts: volumeMounts,
WorkingDir: helper.GetWorkingDir(devbox),
Command: helper.GetCommand(devbox),
Args: helper.GetArgs(devbox),
Resources: helper.GenerateResourceRequirements(devbox, r.RequestRate, r.EphemeralStorage)},
}
terminationGracePeriodSeconds := 300
automountServiceAccountToken := false
runtimeClassName := devbox.Spec.RuntimeClassName
var runtimeClassNamePtr *string
if runtimeClassName == "" {
runtimeClassNamePtr = nil
} else {
runtimeClassNamePtr = ptr.To(runtimeClassName)
}
expectPod := &corev1.Pod{
ObjectMeta: objectMeta,
Spec: corev1.PodSpec{
TerminationGracePeriodSeconds: ptr.To(int64(terminationGracePeriodSeconds)),
AutomountServiceAccountToken: ptr.To(automountServiceAccountToken),
RestartPolicy: corev1.RestartPolicyNever,
Hostname: devbox.Name,
Containers: containers,
Volumes: volumes,
RuntimeClassName: runtimeClassNamePtr,
NodeSelector: devbox.Spec.NodeSelector,
Tolerations: devbox.Spec.Tolerations,
Affinity: devbox.Spec.Affinity,
},
}
// set controller reference and finalizer
_ = controllerutil.SetControllerReference(devbox, expectPod, r.Scheme)
controllerutil.AddFinalizer(expectPod, devboxv1alpha1.FinalizerName)
return expectPod
}
func (r *DevboxReconciler) generateNextCommitHistory(devbox *devboxv1alpha1.Devbox) *devboxv1alpha1.CommitHistory {
now := time.Now()
return &devboxv1alpha1.CommitHistory{
Image: r.generateImageName(devbox),
Time: metav1.Time{Time: now},
Pod: devbox.Name + "-" + rand.String(5),
Status: devboxv1alpha1.CommitStatusPending,
PredicatedStatus: devboxv1alpha1.CommitStatusPending,
}
}
func (r *DevboxReconciler) generateImageName(devbox *devboxv1alpha1.Devbox) string {
now := time.Now()
return fmt.Sprintf("%s/%s/%s:%s-%s", r.CommitImageRegistry, devbox.Namespace, devbox.Name, rand.String(5), now.Format("2006-01-02-150405"))
}
type ControllerRestartPredicate struct {
predicate.Funcs
duration time.Duration
checkTime time.Time
}
func NewControllerRestartPredicate(duration time.Duration) *ControllerRestartPredicate {
return &ControllerRestartPredicate{
checkTime: time.Now().Add(-duration),
duration: duration,
}
}
// skip create event p.duration ago
func (p *ControllerRestartPredicate) Create(e event.CreateEvent) bool {
return e.Object.GetCreationTimestamp().Time.After(p.checkTime)
}
// SetupWithManager sets up the controller with the Manager.
func (r *DevboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
WithOptions(controller.Options{MaxConcurrentReconciles: 10}).
For(&devboxv1alpha1.Devbox{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&corev1.Pod{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). // enqueue request if pod spec/status is updated
Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&corev1.Secret{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
WithEventFilter(NewControllerRestartPredicate(r.RestartPredicateDuration)).
Complete(r)
}