Path: blob/main/components/ws-manager-mk2/controllers/workspace_controller.go
2498 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License-AGPL.txt in the project root for license information.34package controllers56import (7"context"8"fmt"9"strings"10"sync"11"time"1213corev1 "k8s.io/api/core/v1"14"k8s.io/apimachinery/pkg/api/equality"15apierrors "k8s.io/apimachinery/pkg/api/errors"16metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"17"k8s.io/apimachinery/pkg/runtime"18"k8s.io/apimachinery/pkg/types"19"k8s.io/apimachinery/pkg/util/wait"20"k8s.io/apimachinery/pkg/version"21"k8s.io/client-go/kubernetes"22"k8s.io/client-go/rest"23"k8s.io/client-go/tools/record"24"k8s.io/client-go/util/workqueue"25ctrl "sigs.k8s.io/controller-runtime"26"sigs.k8s.io/controller-runtime/pkg/client"27"sigs.k8s.io/controller-runtime/pkg/controller"28"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"29"sigs.k8s.io/controller-runtime/pkg/event"30"sigs.k8s.io/controller-runtime/pkg/handler"31"sigs.k8s.io/controller-runtime/pkg/log"32"sigs.k8s.io/controller-runtime/pkg/predicate"33"sigs.k8s.io/controller-runtime/pkg/reconcile"3435wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"36"github.com/gitpod-io/gitpod/common-go/tracing"37"github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/constants"38"github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/maintenance"39config "github.com/gitpod-io/gitpod/ws-manager/api/config"40workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"41"github.com/go-logr/logr"42"github.com/prometheus/client_golang/prometheus"43)4445const (46metricsNamespace = "gitpod"47metricsWorkspaceSubsystem = "ws_manager_mk2"48// kubernetesOperationTimeout is the time we give Kubernetes operations in general.49kubernetesOperationTimeout = 5 * time.Second50maintenanceRequeue = 1 * time.Minute51)5253func NewWorkspaceReconciler(c client.Client, restConfig *rest.Config, scheme *runtime.Scheme, recorder record.EventRecorder, cfg *config.Configuration, reg prometheus.Registerer, maintenance maintenance.Maintenance) (*WorkspaceReconciler, error) {54// Create kubernetes clientset55kubeClient, err := kubernetes.NewForConfig(restConfig)56if err != nil {57return nil, fmt.Errorf("failed to create kubernetes client: %w", err)58}5960reconciler := &WorkspaceReconciler{61Client: c,62Scheme: scheme,63Config: cfg,64maintenance: maintenance,65Recorder: recorder,66kubeClient: kubeClient,67}6869metrics, err := newControllerMetrics(reconciler)70if err != nil {71return nil, err72}73reg.MustRegister(metrics)74reconciler.metrics = metrics7576return reconciler, nil77}7879// WorkspaceReconciler reconciles a Workspace object80type WorkspaceReconciler struct {81client.Client82Scheme *runtime.Scheme8384Config *config.Configuration85metrics *controllerMetrics86maintenance maintenance.Maintenance87Recorder record.EventRecorder8889kubeClient kubernetes.Interface90}9192//+kubebuilder:rbac:groups=workspace.gitpod.io,resources=workspaces,verbs=get;list;watch;create;update;patch;delete93//+kubebuilder:rbac:groups=workspace.gitpod.io,resources=workspaces/status,verbs=get;update;patch94//+kubebuilder:rbac:groups=workspace.gitpod.io,resources=workspaces/finalizers,verbs=update95//+kubebuilder:rbac:groups=core,resources=pod,verbs=get;list;watch;create;update;patch;delete96//+kubebuilder:rbac:groups=core,resources=pod/status,verbs=get9798// Reconcile is part of the main kubernetes reconciliation loop which aims to99// move the current state of the cluster closer to the desired state.100// Modify the Reconcile function to compare the state specified by101// the Workspace object against the actual cluster state, and then102// perform operations to make the cluster state reflect the state specified by103// the user.104//105// For more details, check Reconcile and its Result here:106// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile107func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {108span, ctx := tracing.FromContext(ctx, "WorkspaceReconciler.Reconcile")109defer tracing.FinishSpan(span, &err)110log := log.FromContext(ctx)111112var workspace workspacev1.Workspace113err = r.Get(ctx, req.NamespacedName, &workspace)114if err != nil {115if !apierrors.IsNotFound(err) {116log.Error(err, "unable to fetch workspace")117}118// we'll ignore not-found errors, since they can't be fixed by an immediate119// requeue (we'll need to wait for a new notification), and we can get them120// on deleted requests.121return ctrl.Result{}, client.IgnoreNotFound(err)122}123124if workspace.Status.Conditions == nil {125workspace.Status.Conditions = []metav1.Condition{}126}127128log = log.WithValues("owi", workspace.OWI())129ctx = logr.NewContext(ctx, log)130log.V(2).Info("reconciling workspace", "workspace", req.NamespacedName, "phase", workspace.Status.Phase)131132workspacePods, err := r.listWorkspacePods(ctx, &workspace)133if err != nil {134log.Error(err, "unable to list workspace pods")135return ctrl.Result{}, fmt.Errorf("failed to list workspace pods: %w", err)136}137138oldStatus := workspace.Status.DeepCopy()139err = r.updateWorkspaceStatus(ctx, &workspace, workspacePods, r.Config)140if err != nil {141return ctrl.Result{}, fmt.Errorf("failed to compute latest workspace status: %w", err)142}143144r.updateMetrics(ctx, &workspace)145r.emitPhaseEvents(ctx, &workspace, oldStatus)146147var podStatus *corev1.PodStatus148if len(workspacePods.Items) > 0 {149podStatus = &workspacePods.Items[0].Status150}151152if !equality.Semantic.DeepDerivative(oldStatus, workspace.Status) {153log.Info("updating workspace status", "status", workspace.Status, "podStatus", podStatus, "pods", len(workspacePods.Items))154}155156err = r.Status().Update(ctx, &workspace)157if err != nil {158return errorResultLogConflict(log, fmt.Errorf("failed to update workspace status: %w", err))159}160161result, err = r.actOnStatus(ctx, &workspace, workspacePods)162if err != nil {163return errorResultLogConflict(log, fmt.Errorf("failed to act on status: %w", err))164}165166return result, nil167}168169func (r *WorkspaceReconciler) listWorkspacePods(ctx context.Context, ws *workspacev1.Workspace) (list *corev1.PodList, err error) {170span, ctx := tracing.FromContext(ctx, "listWorkspacePods")171defer tracing.FinishSpan(span, &err)172173var workspacePods corev1.PodList174err = r.List(ctx, &workspacePods, client.InNamespace(ws.Namespace), client.MatchingFields{wsOwnerKey: ws.Name})175if err != nil {176return nil, err177}178179return &workspacePods, nil180}181182func (r *WorkspaceReconciler) actOnStatus(ctx context.Context, workspace *workspacev1.Workspace, workspacePods *corev1.PodList) (result ctrl.Result, err error) {183span, ctx := tracing.FromContext(ctx, "actOnStatus")184defer tracing.FinishSpan(span, &err)185log := log.FromContext(ctx)186187if workspace.Status.Phase != workspacev1.WorkspacePhaseStopped && !r.metrics.containsWorkspace(workspace) {188// If the workspace hasn't stopped yet, and we don't know about this workspace yet, remember it.189r.metrics.rememberWorkspace(workspace, nil)190}191192if len(workspacePods.Items) == 0 {193// if there isn't a workspace pod and we're not currently deleting this workspace,// create one.194switch {195case workspace.Status.PodStarts == 0 || workspace.Status.PodStarts-workspace.Status.PodRecreated < 1:196serverVersion := r.getServerVersion(ctx)197sctx, err := newStartWorkspaceContext(ctx, r.Config, workspace, serverVersion)198if err != nil {199log.Error(err, "unable to create startWorkspace context")200return ctrl.Result{Requeue: true}, err201}202203pod, err := r.createWorkspacePod(sctx)204if err != nil {205log.Error(err, "unable to produce workspace pod")206return ctrl.Result{}, err207}208209if err := ctrl.SetControllerReference(workspace, pod, r.Scheme); err != nil {210return ctrl.Result{}, err211}212213err = r.Create(ctx, pod)214if apierrors.IsAlreadyExists(err) {215// pod exists, we're good216} else if err != nil {217log.Error(err, "unable to create Pod for Workspace", "pod", pod)218return ctrl.Result{Requeue: true}, err219} else {220// Must increment and persist the pod starts, and ensure we retry on conflict.221// If we fail to persist this value, it's possible that the Pod gets recreated222// when the workspace stops, due to PodStarts still being 0 when the original Pod223// disappears.224// Use a Patch instead of an Update, to prevent conflicts.225patch := client.MergeFrom(workspace.DeepCopy())226workspace.Status.PodStarts++227if err := r.Status().Patch(ctx, workspace, patch); err != nil {228log.Error(err, "Failed to patch PodStarts in workspace status")229return ctrl.Result{}, err230}231232r.Recorder.Event(workspace, corev1.EventTypeNormal, "Creating", "")233}234235case workspace.Status.Phase == workspacev1.WorkspacePhaseStopped && workspace.IsConditionTrue(workspacev1.WorkspaceConditionPodRejected):236if workspace.Status.PodRecreated > r.Config.PodRecreationMaxRetries {237workspace.Status.SetCondition(workspacev1.NewWorkspaceConditionPodRejected(fmt.Sprintf("Pod reached maximum recreations %d, failing", workspace.Status.PodRecreated), metav1.ConditionFalse))238return ctrl.Result{Requeue: true}, nil // requeue so we end up in the "Stopped" case below239}240log = log.WithValues("PodStarts", workspace.Status.PodStarts, "PodRecreated", workspace.Status.PodRecreated, "Phase", workspace.Status.Phase)241242// Make sure to wait for "recreationTimeout" before creating the pod again243if workspace.Status.PodDeletionTime == nil {244log.Info("pod recreation: waiting for pod deletion time to be populated...")245return ctrl.Result{Requeue: true, RequeueAfter: 5 * time.Second}, nil246}247248recreationTimeout := r.podRecreationTimeout()249podDeletionTime := workspace.Status.PodDeletionTime.Time250waitTime := time.Until(podDeletionTime.Add(recreationTimeout))251log = log.WithValues("waitTime", waitTime.String(), "recreationTimeout", recreationTimeout.String(), "podDeletionTime", podDeletionTime.String())252if waitTime > 0 {253log.Info("pod recreation: waiting for timeout...")254return ctrl.Result{Requeue: true, RequeueAfter: waitTime}, nil255}256log.Info("trigger pod recreation")257258// Reset status259sc := workspace.Status.DeepCopy()260workspace.Status = workspacev1.WorkspaceStatus{}261workspace.Status.Phase = workspacev1.WorkspacePhasePending262workspace.Status.OwnerToken = sc.OwnerToken263workspace.Status.PodStarts = sc.PodStarts264workspace.Status.PodRecreated = sc.PodRecreated + 1265workspace.Status.SetCondition(workspacev1.NewWorkspaceConditionPodRejected(fmt.Sprintf("Recreating pod... (%d retry)", workspace.Status.PodRecreated), metav1.ConditionFalse))266267if err := r.Status().Update(ctx, workspace); err != nil {268log.Error(err, "Failed to update workspace status-reset")269return ctrl.Result{}, err270}271272// Reset metrics cache273r.metrics.forgetWorkspace(workspace)274275r.Recorder.Event(workspace, corev1.EventTypeNormal, "Recreating", "")276return ctrl.Result{Requeue: true}, nil277278case workspace.Status.Phase == workspacev1.WorkspacePhaseStopped:279if err := r.deleteWorkspaceSecrets(ctx, workspace); err != nil {280return ctrl.Result{}, err281}282283// Done stopping workspace - remove finalizer.284if controllerutil.ContainsFinalizer(workspace, workspacev1.GitpodFinalizerName) {285controllerutil.RemoveFinalizer(workspace, workspacev1.GitpodFinalizerName)286if err := r.Update(ctx, workspace); err != nil {287if apierrors.IsNotFound(err) {288return ctrl.Result{}, nil289} else {290return ctrl.Result{}, fmt.Errorf("failed to remove gitpod finalizer from workspace: %w", err)291}292}293}294295// Workspace might have already been in a deleting state,296// but not guaranteed, so try deleting anyway.297r.Recorder.Event(workspace, corev1.EventTypeNormal, "Deleting", "")298err := r.Client.Delete(ctx, workspace)299return ctrl.Result{}, client.IgnoreNotFound(err)300}301302return ctrl.Result{}, nil303}304305// all actions below assume there is a pod306if len(workspacePods.Items) == 0 {307return ctrl.Result{}, nil308}309pod := &workspacePods.Items[0]310311switch {312// if there is a pod, and it's failed, delete it313case workspace.IsConditionTrue(workspacev1.WorkspaceConditionFailed) && !isPodBeingDeleted(pod):314return r.deleteWorkspacePod(ctx, pod, "workspace failed")315316// if the pod was stopped by request, delete it317case workspace.IsConditionTrue(workspacev1.WorkspaceConditionStoppedByRequest) && !isPodBeingDeleted(pod):318var gracePeriodSeconds *int64319if c := wsk8s.GetCondition(workspace.Status.Conditions, string(workspacev1.WorkspaceConditionStoppedByRequest)); c != nil {320if dt, err := time.ParseDuration(c.Message); err == nil {321s := int64(dt.Seconds())322gracePeriodSeconds = &s323}324}325err := r.Client.Delete(ctx, pod, &client.DeleteOptions{326GracePeriodSeconds: gracePeriodSeconds,327})328if apierrors.IsNotFound(err) {329// pod is gone - nothing to do here330} else {331return ctrl.Result{Requeue: true}, err332}333334// if the node disappeared, delete the pod.335case workspace.IsConditionTrue(workspacev1.WorkspaceConditionNodeDisappeared) && !isPodBeingDeleted(pod):336return r.deleteWorkspacePod(ctx, pod, "node disappeared")337338// if the workspace timed out, delete it339case workspace.IsConditionTrue(workspacev1.WorkspaceConditionTimeout) && !isPodBeingDeleted(pod):340return r.deleteWorkspacePod(ctx, pod, "timed out")341342// if the content initialization failed, delete the pod343case wsk8s.ConditionWithStatusAndReason(workspace.Status.Conditions, string(workspacev1.WorkspaceConditionContentReady), false, workspacev1.ReasonInitializationFailure) && !isPodBeingDeleted(pod):344return r.deleteWorkspacePod(ctx, pod, "init failed")345346case isWorkspaceBeingDeleted(workspace) && !isPodBeingDeleted(pod):347return r.deleteWorkspacePod(ctx, pod, "workspace deleted")348349case workspace.IsHeadless() && workspace.Status.Phase == workspacev1.WorkspacePhaseStopped && !isPodBeingDeleted(pod):350// Workspace was requested to be deleted, propagate by deleting the Pod.351// The Pod deletion will then trigger workspace disposal steps.352err := r.Client.Delete(ctx, pod)353if apierrors.IsNotFound(err) {354// pod is gone - nothing to do here355} else {356return ctrl.Result{Requeue: true}, err357}358359case workspace.Status.Phase == workspacev1.WorkspacePhaseRunning:360err := r.deleteWorkspaceSecrets(ctx, workspace)361if err != nil {362log.Error(err, "could not delete workspace secrets")363}364365// we've disposed already - try to remove the finalizer and call it a day366case workspace.Status.Phase == workspacev1.WorkspacePhaseStopped:367hadFinalizer := controllerutil.ContainsFinalizer(pod, workspacev1.GitpodFinalizerName)368controllerutil.RemoveFinalizer(pod, workspacev1.GitpodFinalizerName)369if err := r.Client.Update(ctx, pod); err != nil {370return ctrl.Result{}, fmt.Errorf("failed to remove gitpod finalizer from pod: %w", err)371}372373if hadFinalizer {374// Requeue to remove workspace.375return ctrl.Result{RequeueAfter: 10 * time.Second}, nil376}377}378379return ctrl.Result{}, nil380}381382func (r *WorkspaceReconciler) podRecreationTimeout() time.Duration {383recreationTimeout := 15 * time.Second // waiting less time creates issues with ws-daemon's pod-centric control loop ("Dispatch") if the workspace ends up on the same node again384if r.Config.PodRecreationBackoff != 0 {385recreationTimeout = time.Duration(r.Config.PodRecreationBackoff)386}387return recreationTimeout388}389390func (r *WorkspaceReconciler) updateMetrics(ctx context.Context, workspace *workspacev1.Workspace) {391log := log.FromContext(ctx)392393ok, lastState := r.metrics.getWorkspace(&log, workspace)394if !ok {395return396}397398if !lastState.recordedInitFailure && wsk8s.ConditionWithStatusAndReason(workspace.Status.Conditions, string(workspacev1.WorkspaceConditionContentReady), false, workspacev1.ReasonInitializationFailure) {399r.metrics.countTotalRestoreFailures(&log, workspace)400lastState.recordedInitFailure = true401}402403if !lastState.recordedFailure && workspace.IsConditionTrue(workspacev1.WorkspaceConditionFailed) {404r.metrics.countWorkspaceFailure(&log, workspace)405lastState.recordedFailure = true406}407408if lastState.pendingStartTime.IsZero() && workspace.Status.Phase == workspacev1.WorkspacePhasePending {409lastState.pendingStartTime = time.Now()410} else if !lastState.pendingStartTime.IsZero() && workspace.Status.Phase != workspacev1.WorkspacePhasePending {411r.metrics.recordWorkspacePendingTime(&log, workspace, lastState.pendingStartTime)412lastState.pendingStartTime = time.Time{}413}414415if lastState.creatingStartTime.IsZero() && workspace.Status.Phase == workspacev1.WorkspacePhaseCreating {416lastState.creatingStartTime = time.Now()417} else if !lastState.creatingStartTime.IsZero() && workspace.Status.Phase != workspacev1.WorkspacePhaseCreating {418r.metrics.recordWorkspaceCreatingTime(&log, workspace, lastState.creatingStartTime)419lastState.creatingStartTime = time.Time{}420}421422if !lastState.recordedContentReady && workspace.IsConditionTrue(workspacev1.WorkspaceConditionContentReady) {423r.metrics.countTotalRestores(&log, workspace)424lastState.recordedContentReady = true425}426427if !lastState.recordedBackupFailed && workspace.IsConditionTrue(workspacev1.WorkspaceConditionBackupFailure) {428r.metrics.countTotalBackups(&log, workspace)429r.metrics.countTotalBackupFailures(&log, workspace)430lastState.recordedBackupFailed = true431}432433if !lastState.recordedBackupCompleted && workspace.IsConditionTrue(workspacev1.WorkspaceConditionBackupComplete) {434r.metrics.countTotalBackups(&log, workspace)435lastState.recordedBackupCompleted = true436}437438if !lastState.recordedStartTime && workspace.Status.Phase == workspacev1.WorkspacePhaseRunning {439r.metrics.recordWorkspaceStartupTime(&log, workspace)440lastState.recordedStartTime = true441}442443if lastState.recordedRecreations < workspace.Status.PodRecreated {444r.metrics.countWorkspaceRecreations(&log, workspace)445lastState.recordedRecreations = workspace.Status.PodRecreated446}447448if workspace.Status.Phase == workspacev1.WorkspacePhaseStopped {449r.metrics.countWorkspaceStop(&log, workspace)450451if !lastState.recordedStartFailure && isStartFailure(workspace) {452// Workspace never became ready, count as a startup failure.453r.metrics.countWorkspaceStartFailures(&log, workspace)454// No need to record in metricState, as we're forgetting the workspace state next anyway.455}456457// Forget about this workspace, no more state updates will be recorded after this.458r.metrics.forgetWorkspace(workspace)459return460}461462r.metrics.rememberWorkspace(workspace, &lastState)463}464465func isStartFailure(ws *workspacev1.Workspace) bool {466// Consider workspaces that never became ready as start failures.467everReady := ws.IsConditionTrue(workspacev1.WorkspaceConditionEverReady)468// Except for aborted prebuilds, as they can get aborted before becoming ready, which shouldn't be counted469// as a start failure.470isAborted := ws.IsConditionTrue(workspacev1.WorkspaceConditionAborted)471// Also ignore workspaces that are requested to be stopped before they became ready.472isStoppedByRequest := ws.IsConditionTrue(workspacev1.WorkspaceConditionStoppedByRequest)473// Also ignore pods that got rejected by the node474isPodRejected := ws.IsConditionTrue(workspacev1.WorkspaceConditionPodRejected)475return !everReady && !isAborted && !isStoppedByRequest && !isPodRejected476}477478func (r *WorkspaceReconciler) emitPhaseEvents(ctx context.Context, ws *workspacev1.Workspace, old *workspacev1.WorkspaceStatus) {479if ws.Status.Phase == workspacev1.WorkspacePhaseInitializing && old.Phase != workspacev1.WorkspacePhaseInitializing {480r.Recorder.Event(ws, corev1.EventTypeNormal, "Initializing", "")481}482483if ws.Status.Phase == workspacev1.WorkspacePhaseRunning && old.Phase != workspacev1.WorkspacePhaseRunning {484r.Recorder.Event(ws, corev1.EventTypeNormal, "Running", "")485}486487if ws.Status.Phase == workspacev1.WorkspacePhaseStopping && old.Phase != workspacev1.WorkspacePhaseStopping {488r.Recorder.Event(ws, corev1.EventTypeNormal, "Stopping", "")489}490}491492func (r *WorkspaceReconciler) deleteWorkspacePod(ctx context.Context, pod *corev1.Pod, reason string) (result ctrl.Result, err error) {493span, ctx := tracing.FromContext(ctx, "deleteWorkspacePod")494defer tracing.FinishSpan(span, &err)495496// Workspace was requested to be deleted, propagate by deleting the Pod.497// The Pod deletion will then trigger workspace disposal steps.498err = r.Client.Delete(ctx, pod)499if apierrors.IsNotFound(err) {500// pod is gone - nothing to do here501} else {502return ctrl.Result{Requeue: true}, err503}504505return ctrl.Result{}, nil506}507508func (r *WorkspaceReconciler) deleteWorkspaceSecrets(ctx context.Context, ws *workspacev1.Workspace) (err error) {509span, ctx := tracing.FromContext(ctx, "deleteWorkspaceSecrets")510defer tracing.FinishSpan(span, &err)511log := log.FromContext(ctx).WithValues("owi", ws.OWI())512513// if a secret cannot be deleted we do not return early because we want to attempt514// the deletion of the remaining secrets515var errs []string516err = r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "env"), r.Config.Namespace)517if err != nil {518errs = append(errs, err.Error())519log.Error(err, "could not delete environment secret", "workspace", ws.Name)520}521522err = r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "tokens"), r.Config.SecretsNamespace)523if err != nil {524errs = append(errs, err.Error())525log.Error(err, "could not delete token secret", "workspace", ws.Name)526}527528if len(errs) != 0 {529return fmt.Errorf("%s", strings.Join(errs, ":"))530}531532return nil533}534535func (r *WorkspaceReconciler) deleteSecret(ctx context.Context, name, namespace string) error {536log := log.FromContext(ctx)537538err := wait.ExponentialBackoffWithContext(ctx, wait.Backoff{539Duration: 100 * time.Millisecond,540Factor: 1.5,541Jitter: 0.2,542Steps: 3,543}, func(ctx context.Context) (bool, error) {544var secret corev1.Secret545err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &secret)546if apierrors.IsNotFound(err) {547// nothing to delete548return true, nil549}550551if err != nil {552log.Error(err, "cannot retrieve secret scheduled for deletion", "secret", name)553return false, nil554}555556err = r.Client.Delete(ctx, &secret)557if err != nil && !apierrors.IsNotFound(err) {558log.Error(err, "cannot delete secret", "secret", name)559return false, nil560}561562return true, nil563})564565return err566}567568// errorLogConflict logs the error if it's a conflict, instead of returning it as a reconciler error.569// This is to reduce noise in our error logging, as conflicts are to be expected.570// For conflicts, instead a result with `Requeue: true` is returned, which has the same requeuing571// behaviour as returning an error.572func errorResultLogConflict(log logr.Logger, err error) (ctrl.Result, error) {573if apierrors.IsConflict(err) {574return ctrl.Result{RequeueAfter: 100 * time.Millisecond}, nil575} else {576return ctrl.Result{}, err577}578}579580var (581wsOwnerKey = ".metadata.controller"582apiGVStr = workspacev1.GroupVersion.String()583)584585// SetupWithManager sets up the controller with the Manager.586func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {587return ctrl.NewControllerManagedBy(mgr).588Named("workspace").589WithOptions(controller.Options{590MaxConcurrentReconciles: r.Config.WorkspaceMaxConcurrentReconciles,591}).592For(&workspacev1.Workspace{}).593WithEventFilter(predicate.NewPredicateFuncs(func(object client.Object) bool {594_, ok := object.(*corev1.Node)595if ok {596return true597}598599for k, v := range object.GetLabels() {600if k == wsk8s.WorkspaceManagedByLabel {601switch v {602case constants.ManagedBy:603return true604default:605return false606}607}608}609610return true611})).612Owns(&corev1.Pod{}).613// Add a watch for Nodes, so that they're cached in memory and don't require calling the k8s API614// when reconciling workspaces.615Watches(&corev1.Node{}, &handler.Funcs{616// Only enqueue events for workspaces when the node gets deleted,617// such that we can trigger their cleanup.618DeleteFunc: func(ctx context.Context, e event.TypedDeleteEvent[client.Object], queue workqueue.TypedRateLimitingInterface[reconcile.Request]) {619if e.Object == nil {620return621}622623var wsList workspacev1.WorkspaceList624err := r.List(ctx, &wsList)625if err != nil {626log.FromContext(ctx).Error(err, "cannot list workspaces")627return628}629for _, ws := range wsList.Items {630if ws.Status.Runtime == nil || ws.Status.Runtime.NodeName != e.Object.GetName() {631continue632}633queue.Add(ctrl.Request{NamespacedName: types.NamespacedName{634Namespace: ws.Namespace,635Name: ws.Name,636}})637}638},639}).640Complete(r)641}642643func (r *WorkspaceReconciler) getServerVersion(ctx context.Context) *version.Info {644log := log.FromContext(ctx)645646serverVersion, err := r.kubeClient.Discovery().ServerVersion()647if err != nil {648log.Error(err, "cannot get server version! Assuming 1.30 going forward")649serverVersion = &version.Info{650Major: "1",651Minor: "30",652}653}654return serverVersion655}656657func SetupIndexer(mgr ctrl.Manager) error {658var err error659var once sync.Once660once.Do(func() {661idx := func(rawObj client.Object) []string {662// grab the job object, extract the owner...663job := rawObj.(*corev1.Pod)664owner := metav1.GetControllerOf(job)665if owner == nil {666return nil667}668// ...make sure it's a workspace...669if owner.APIVersion != apiGVStr || owner.Kind != "Workspace" {670return nil671}672673// ...and if so, return it674return []string{owner.Name}675}676677err = mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, wsOwnerKey, idx)678})679680return err681}682683684