mirror of
https://github.com/labring/sealos.git
synced 2026-09-19 02:23:07 +08:00
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>
743 lines
26 KiB
Go
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)
|
|
}
|