Skip to content

Commit e441397

Browse files
committed
fix: require resourceVersion on workspace updates and validate coder URL
1 parent b736502 commit e441397

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

app_dispatch.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"flag"
66
"fmt"
7+
"net/url"
8+
"strings"
79
"time"
810

911
ctrl "sigs.k8s.io/controller-runtime"
@@ -62,6 +64,23 @@ func run(args []string) error {
6264
return err
6365
}
6466

67+
if coderURL != "" {
68+
parsedCoderURL, err := url.Parse(coderURL)
69+
if err != nil {
70+
return fmt.Errorf("assertion failed: invalid --coder-url %q: %w", coderURL, err)
71+
}
72+
if parsedCoderURL.Scheme == "" || parsedCoderURL.Host == "" {
73+
return fmt.Errorf(
74+
"assertion failed: invalid --coder-url %q: must include scheme and host (for example, https://coder.example.com)",
75+
coderURL,
76+
)
77+
}
78+
scheme := strings.ToLower(parsedCoderURL.Scheme)
79+
if scheme != "http" && scheme != "https" {
80+
return fmt.Errorf("assertion failed: invalid --coder-url %q: scheme must be http or https", coderURL)
81+
}
82+
}
83+
6584
switch appMode {
6685
case "controller":
6786
return runControllerApp(setupSignalHandler())

internal/aggregated/storage/storage_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,52 @@ func TestWorkspaceStorageUpdateRejectsDifferentTemplateVersionID(t *testing.T) {
692692
}
693693
}
694694

695+
func TestWorkspaceStorageUpdateRejectsMissingResourceVersion(t *testing.T) {
696+
t.Parallel()
697+
698+
server, state := newMockCoderServer(t)
699+
defer server.Close()
700+
701+
workspaceStorage := NewWorkspaceStorage(newTestClientProvider(t, server.URL))
702+
ctx := namespacedContext("control-plane")
703+
704+
currentObj, err := workspaceStorage.Get(ctx, "acme.alice.dev-workspace", nil)
705+
if err != nil {
706+
t.Fatalf("expected workspace get to succeed: %v", err)
707+
}
708+
709+
currentWorkspace, ok := currentObj.(*aggregationv1alpha1.CoderWorkspace)
710+
if !ok {
711+
t.Fatalf("expected *CoderWorkspace from get, got %T", currentObj)
712+
}
713+
if currentWorkspace.ResourceVersion == "" {
714+
t.Fatal("expected current workspace resourceVersion to be populated")
715+
}
716+
717+
desiredWorkspace := currentWorkspace.DeepCopy()
718+
desiredWorkspace.Spec.Running = !currentWorkspace.Spec.Running
719+
desiredWorkspace.ResourceVersion = ""
720+
721+
_, _, err = workspaceStorage.Update(
722+
ctx,
723+
desiredWorkspace.Name,
724+
testUpdatedObjectInfo{obj: desiredWorkspace},
725+
nil,
726+
rest.ValidateAllObjectUpdateFunc,
727+
false,
728+
nil,
729+
)
730+
if !apierrors.IsBadRequest(err) {
731+
t.Fatalf("expected BadRequest for missing resourceVersion, got %v", err)
732+
}
733+
if err == nil || !strings.Contains(err.Error(), "metadata.resourceVersion is required for update") {
734+
t.Fatalf("expected missing resourceVersion error message, got %v", err)
735+
}
736+
if transitions := state.buildTransitionsSnapshot(); len(transitions) != 0 {
737+
t.Fatalf("expected no workspace build transitions when resourceVersion is missing, got %v", transitions)
738+
}
739+
}
740+
695741
func TestWorkspaceStorageUpdateRejectsStaleResourceVersion(t *testing.T) {
696742
t.Parallel()
697743

internal/aggregated/storage/workspace.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,10 @@ func (s *WorkspaceStorage) Update(
333333
fmt.Sprintf("metadata.namespace %q does not match request namespace %q", desiredObj.Namespace, namespace),
334334
)
335335
}
336-
if desiredObj.ResourceVersion != "" && desiredObj.ResourceVersion != currentK8sObj.ResourceVersion {
336+
if desiredObj.ResourceVersion == "" {
337+
return nil, false, apierrors.NewBadRequest("metadata.resourceVersion is required for update")
338+
}
339+
if desiredObj.ResourceVersion != currentK8sObj.ResourceVersion {
337340
return nil, false, apierrors.NewConflict(
338341
aggregationv1alpha1.Resource("coderworkspaces"),
339342
name,

main_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,66 @@ func TestRunDispatchesAggregatedAPIServerMode(t *testing.T) {
139139
}
140140
}
141141

142+
func TestRunRejectsAggregatedAPIServerModeWithCoderURLMissingScheme(t *testing.T) {
143+
t.Helper()
144+
installMockSignalHandler(t)
145+
146+
previous := runAggregatedAPIServerApp
147+
t.Cleanup(func() {
148+
runAggregatedAPIServerApp = previous
149+
})
150+
151+
called := false
152+
runAggregatedAPIServerApp = func(context.Context, apiserverapp.Options) error {
153+
called = true
154+
return nil
155+
}
156+
157+
err := run([]string{
158+
"--app=aggregated-apiserver",
159+
"--coder-url=coder.example.com",
160+
})
161+
if err == nil {
162+
t.Fatal("expected an error when --coder-url omits scheme")
163+
}
164+
if !strings.Contains(err.Error(), "must include scheme and host") {
165+
t.Fatalf("expected missing scheme/host validation error, got %v", err)
166+
}
167+
if called {
168+
t.Fatal("expected aggregated apiserver runner not to be called on invalid --coder-url")
169+
}
170+
}
171+
172+
func TestRunRejectsAggregatedAPIServerModeWithUnsupportedCoderURLScheme(t *testing.T) {
173+
t.Helper()
174+
installMockSignalHandler(t)
175+
176+
previous := runAggregatedAPIServerApp
177+
t.Cleanup(func() {
178+
runAggregatedAPIServerApp = previous
179+
})
180+
181+
called := false
182+
runAggregatedAPIServerApp = func(context.Context, apiserverapp.Options) error {
183+
called = true
184+
return nil
185+
}
186+
187+
err := run([]string{
188+
"--app=aggregated-apiserver",
189+
"--coder-url=ftp://coder.example.com",
190+
})
191+
if err == nil {
192+
t.Fatal("expected an error when --coder-url has unsupported scheme")
193+
}
194+
if !strings.Contains(err.Error(), "scheme must be http or https") {
195+
t.Fatalf("expected scheme validation error, got %v", err)
196+
}
197+
if called {
198+
t.Fatal("expected aggregated apiserver runner not to be called on invalid --coder-url")
199+
}
200+
}
201+
142202
func TestRunDispatchesMCPHTTPMode(t *testing.T) {
143203
t.Helper()
144204
installMockSignalHandler(t)

0 commit comments

Comments
 (0)