Skip to content

Commit 7c277f1

Browse files
fix(pro): always check for available template updates and wait for
changed parameters to be applied Changes the template behaviour for pro instances like so: 1. If template is not versioned _always_ keep the workspace in sync with template, even if that means rescheduling due to workspace changes 2. If the template is versioned and the workspace uses an explicit version we keep the workspace up to date with the template unless a new version is available. We expect versioned templates to introduce changes through a new version, not by updating an existing version In addition to that we now wait until the server applied parameter changes instead of assuming it did in time. This fixes a race condition where the parameter changes wouldn't be applied if the controller took more than a couple ms
1 parent fafc66a commit 7c277f1

4 files changed

Lines changed: 121 additions & 24 deletions

File tree

cmd/pro/provider/up.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@ package provider
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"io"
78
"os"
89

910
"github.com/loft-sh/devpod/cmd/pro/flags"
11+
"github.com/loft-sh/devpod/pkg/client/clientimplementation"
1012
"github.com/loft-sh/devpod/pkg/platform"
1113
"github.com/loft-sh/devpod/pkg/platform/client"
1214
"github.com/loft-sh/devpod/pkg/platform/remotecommand"
1315
"github.com/loft-sh/log"
16+
"github.com/sirupsen/logrus"
1417
"github.com/spf13/cobra"
1518

1619
managementv1 "github.com/loft-sh/api/v4/pkg/apis/management/v1"
1720
storagev1 "github.com/loft-sh/api/v4/pkg/apis/storage/v1"
21+
corev1 "k8s.io/api/core/v1"
1822
)
1923

2024
// UpCmd holds the cmd flags:
@@ -33,9 +37,15 @@ type streams struct {
3337

3438
// NewUpCmd creates a new command
3539
func NewUpCmd(globalFlags *flags.GlobalFlags) *cobra.Command {
40+
logLevel := logrus.InfoLevel
41+
if os.Getenv(clientimplementation.DevPodDebug) == "true" || globalFlags.Debug {
42+
logLevel = logrus.DebugLevel
43+
}
44+
3645
cmd := &UpCmd{
3746
GlobalFlags: globalFlags,
38-
Log: log.GetInstance(),
47+
Log: log.NewStreamLoggerWithFormat( /* we don't use stdout */ nil,
48+
os.Stderr, logLevel, log.JSONFormat).ErrorStreamOnly(),
3949
streams: streams{
4050
Stdin: os.Stdin,
4151
Stdout: os.Stdout,
@@ -69,6 +79,24 @@ func (cmd *UpCmd) Run(ctx context.Context) error {
6979
instance, err := platform.FindInstanceInProject(ctx, baseClient, info.UID, info.ProjectName)
7080
if err != nil {
7181
return err
82+
} else if instance == nil {
83+
return fmt.Errorf("workspace %s not found in project %s. Looks like it does not exist anymore and you can delete it", info.ID, info.ProjectName)
84+
}
85+
86+
// Log current workspace information. This is both useful to the user to understand the workspace configuration
87+
// and to us when we receive troubleshooting logs
88+
printInstanceInfo(instance, cmd.Log)
89+
90+
if instance.Spec.TemplateRef != nil && templateUpdateRequired(instance) {
91+
cmd.Log.Info("Template update required")
92+
oldInstance := instance.DeepCopy()
93+
instance.Spec.TemplateRef.SyncOnce = true
94+
95+
instance, err = platform.UpdateInstance(ctx, baseClient, oldInstance, instance, cmd.Log)
96+
if err != nil {
97+
return fmt.Errorf("update instance: %w", err)
98+
}
99+
cmd.Log.Info("Successfully updated template")
72100
}
73101

74102
return cmd.up(ctx, instance, baseClient)
@@ -85,10 +113,41 @@ func (cmd *UpCmd) up(ctx context.Context, workspace *managementv1.DevPodWorkspac
85113
return err
86114
}
87115

88-
_, err = remotecommand.ExecuteConn(ctx, conn, cmd.streams.Stdin, cmd.streams.Stdout, cmd.streams.Stderr, cmd.Log.ErrorStreamOnly())
116+
_, err = remotecommand.ExecuteConn(ctx, conn, cmd.streams.Stdin, cmd.streams.Stdout, cmd.streams.Stderr, cmd.Log)
89117
if err != nil {
90118
return fmt.Errorf("error executing: %w", err)
91119
}
92120

93121
return nil
94122
}
123+
124+
func templateUpdateRequired(instance *managementv1.DevPodWorkspaceInstance) bool {
125+
var templateResolved, templateChangesAvailable bool
126+
for _, condition := range instance.Status.Conditions {
127+
if condition.Type == storagev1.InstanceTemplateResolved {
128+
templateResolved = condition.Status == corev1.ConditionTrue
129+
continue
130+
}
131+
132+
if condition.Type == storagev1.InstanceTemplateSynced {
133+
templateChangesAvailable = condition.Status == corev1.ConditionFalse &&
134+
condition.Reason == "TemplateChangesAvailable"
135+
continue
136+
}
137+
}
138+
139+
return !templateResolved || templateChangesAvailable
140+
}
141+
142+
func printInstanceInfo(instance *managementv1.DevPodWorkspaceInstance, log log.Logger) {
143+
workspaceConfig, _ := json.Marshal(struct {
144+
Runner storagev1.RunnerRef
145+
Template *storagev1.TemplateRef
146+
Parameters string
147+
}{
148+
Runner: instance.Spec.RunnerRef,
149+
Template: instance.Spec.TemplateRef,
150+
Parameters: instance.Spec.Parameters,
151+
})
152+
log.Info("Starting pro workspace with configuration", string(workspaceConfig))
153+
}

cmd/pro/provider/update/workspace.go

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/loft-sh/log/terminal"
1818
"github.com/spf13/cobra"
1919
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20-
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
2120
)
2221

2322
// WorkspaceCmd holds the cmd flags
@@ -112,25 +111,10 @@ func (cmd *WorkspaceCmd) Run(ctx context.Context, stdin io.Reader, stdout io.Wri
112111
}
113112

114113
func updateInstance(ctx context.Context, client client.Client, oldInstance *managementv1.DevPodWorkspaceInstance, newInstance *managementv1.DevPodWorkspaceInstance, log log.Logger) (*managementv1.DevPodWorkspaceInstance, error) {
115-
managementClient, err := client.Management()
116-
if err != nil {
117-
return nil, err
118-
}
119-
120-
patch := ctrlclient.MergeFrom(oldInstance)
121-
data, err := patch.Data(newInstance)
122-
if err != nil {
123-
return nil, err
124-
} else if len(data) == 0 || string(data) == "{}" {
125-
return newInstance, nil
126-
}
127-
128-
res, err := managementClient.Loft().ManagementV1().
129-
DevPodWorkspaceInstances(oldInstance.GetNamespace()).
130-
Patch(ctx, oldInstance.GetName(), patch.Type(), data, metav1.PatchOptions{})
131-
if err != nil {
132-
return nil, err
114+
// This ensures the template is kept up to date with configuration changes
115+
if newInstance.Spec.TemplateRef != nil {
116+
newInstance.Spec.TemplateRef.SyncOnce = true
133117
}
134118

135-
return platform.WaitForInstance(ctx, client, res, log)
119+
return platform.UpdateInstance(ctx, client, oldInstance, newInstance, log)
136120
}

pkg/ide/vscode/open.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func openViaCLI(ctx context.Context, workspace, folder string, newWindow bool, f
109109
// Needs to be separated by `=` because of windows
110110
folderUriArg := fmt.Sprintf("--folder-uri=vscode-remote://ssh-remote+%s.devpod/%s", workspace, folder)
111111
args = append(args, folderUriArg)
112-
log.Debugf("Run %s command %s %s", flavor, codePath, strings.Join(args, " "))
112+
log.Debugf("Run %s command %s %s", flavor.DisplayName(), codePath, strings.Join(args, " "))
113113
out, err = exec.CommandContext(ctx, codePath, args...).CombinedOutput()
114114
if err != nil {
115115
return command.WrapCommandError(out, err)

pkg/platform/instance.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import (
1717
"github.com/loft-sh/devpod/pkg/platform/client"
1818
"github.com/loft-sh/devpod/pkg/platform/project"
1919
"github.com/loft-sh/log"
20+
corev1 "k8s.io/api/core/v1"
2021
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122
"k8s.io/apimachinery/pkg/util/wait"
23+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
2224
)
2325

2426
type WorkspaceInfo struct {
@@ -159,6 +161,32 @@ func DialInstance(baseClient client.Client, workspace *managementv1.DevPodWorksp
159161
return conn, nil
160162
}
161163

164+
// UpdateInstance diffs two versions of a DevPodWorkspaceInstance, applies changes via a patch to reduce conflicts.
165+
// Afterwards it waits until the instance is ready to be used.
166+
func UpdateInstance(ctx context.Context, client client.Client, oldInstance *managementv1.DevPodWorkspaceInstance, newInstance *managementv1.DevPodWorkspaceInstance, log log.Logger) (*managementv1.DevPodWorkspaceInstance, error) {
167+
managementClient, err := client.Management()
168+
if err != nil {
169+
return nil, err
170+
}
171+
172+
patch := ctrlclient.MergeFrom(oldInstance)
173+
data, err := patch.Data(newInstance)
174+
if err != nil {
175+
return nil, err
176+
} else if len(data) == 0 || string(data) == "{}" {
177+
return newInstance, nil
178+
}
179+
180+
res, err := managementClient.Loft().ManagementV1().
181+
DevPodWorkspaceInstances(oldInstance.GetNamespace()).
182+
Patch(ctx, oldInstance.GetName(), patch.Type(), data, metav1.PatchOptions{})
183+
if err != nil {
184+
return nil, err
185+
}
186+
187+
return WaitForInstance(ctx, client, res, log)
188+
}
189+
162190
func WaitForInstance(ctx context.Context, client client.Client, instance *managementv1.DevPodWorkspaceInstance, log log.Logger) (*managementv1.DevPodWorkspaceInstance, error) {
163191
managementClient, err := client.Management()
164192
if err != nil {
@@ -182,8 +210,18 @@ func WaitForInstance(ctx context.Context, client client.Client, instance *manage
182210
return false, nil
183211
}
184212

213+
if !isTemplateSynced(updatedInstance) {
214+
log.Debugf("Workspace template is not ready yet")
215+
for _, cond := range updatedInstance.Status.Conditions {
216+
if cond.Status != corev1.ConditionTrue {
217+
log.Debugf("%s is %s (%s): %s", cond.Type, cond.Status, cond.Reason, cond.Message)
218+
}
219+
}
220+
return false, nil
221+
}
222+
185223
if !isRunnerReady(updatedInstance, storagev1.BuiltinRunnerName) {
186-
log.Debugf("Runner is not ready yet, waiting until its ready", name, status.Phase)
224+
log.Debugf("Runner is not ready yet", name, status.Phase)
187225
return false, nil
188226
}
189227

@@ -218,3 +256,19 @@ func isRunnerReady(workspace *managementv1.DevPodWorkspaceInstance, builtinRunne
218256
return workspace.GetAnnotations() != nil &&
219257
workspace.GetAnnotations()[storagev1.DevPodWorkspaceRunnerEndpointAnnotation] != ""
220258
}
259+
260+
func isTemplateSynced(workspace *managementv1.DevPodWorkspaceInstance) bool {
261+
// We're still waiting for the sync to happen
262+
// The controller will remove this field once it's done syncing
263+
if workspace.Spec.TemplateRef != nil && workspace.Spec.TemplateRef.SyncOnce {
264+
return false
265+
}
266+
267+
for _, condition := range workspace.Status.Conditions {
268+
if condition.Type == storagev1.InstanceTemplateResolved {
269+
return condition.Status == corev1.ConditionTrue
270+
}
271+
}
272+
273+
return false
274+
}

0 commit comments

Comments
 (0)