diff --git a/doc/wbctl cancel job-submission.md b/doc/wbctl cancel job-submission.md index a16ebc1..2ce00b2 100644 --- a/doc/wbctl cancel job-submission.md +++ b/doc/wbctl cancel job-submission.md @@ -1,5 +1,5 @@
-Cancels the Job-Submission resources identified by a list of resource ids. At least one id is required. +Cancels the Job-Submission resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl cancel job-submission [flags] diff --git a/doc/wbctl delete images.md b/doc/wbctl delete images.md index 3506a91..639f992 100644 --- a/doc/wbctl delete images.md +++ b/doc/wbctl delete images.md @@ -1,5 +1,5 @@-Deletes the Image resources identified by a list of resource ids. At least one id is required. +Deletes the Image resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl delete images [flags] diff --git a/doc/wbctl delete job-submissions.md b/doc/wbctl delete job-submissions.md index f57efa2..24266fd 100644 --- a/doc/wbctl delete job-submissions.md +++ b/doc/wbctl delete job-submissions.md @@ -1,5 +1,5 @@-Deletes the Job-Submission resources identified by a list of resource ids. At least one id is required. +Deletes the Job-Submission resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl delete job-submissions [flags] diff --git a/doc/wbctl delete jobs.md b/doc/wbctl delete jobs.md index e650edd..7a93307 100644 --- a/doc/wbctl delete jobs.md +++ b/doc/wbctl delete jobs.md @@ -1,5 +1,5 @@-Deletes the Job resources identified by a list of resource ids. At least one id is required. +Deletes the Job resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl delete jobs [flags] diff --git a/doc/wbctl delete snapshots.md b/doc/wbctl delete snapshots.md index e637b8d..8026375 100644 --- a/doc/wbctl delete snapshots.md +++ b/doc/wbctl delete snapshots.md @@ -1,5 +1,5 @@-Deletes the Snapshot resources identified by a list of resource ids. At least one id is required. +Deletes the Snapshot resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl delete snapshots [flags] diff --git a/doc/wbctl delete volumes.md b/doc/wbctl delete volumes.md index 87b8cbe..64da9d7 100644 --- a/doc/wbctl delete volumes.md +++ b/doc/wbctl delete volumes.md @@ -1,5 +1,5 @@-Deletes the Volume resources identified by a list of resource ids. At least one id is required. +Deletes the Volume resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl delete volumes [flags] diff --git a/doc/wbctl delete workbenches.md b/doc/wbctl delete workbenches.md index 7537480..87ec2ad 100644 --- a/doc/wbctl delete workbenches.md +++ b/doc/wbctl delete workbenches.md @@ -1,5 +1,5 @@-Deletes the Workbench resources identified by a list of resource ids. At least one id is required. +Deletes the Workbench resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl delete workbenches [flags] diff --git a/doc/wbctl describe image-builds.md b/doc/wbctl describe image-builds.md index 55de127..a6ff774 100644 --- a/doc/wbctl describe image-builds.md +++ b/doc/wbctl describe image-builds.md @@ -1,5 +1,5 @@-Print detailed information about the Image-Build resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Image-Build resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe image-builds [flags] diff --git a/doc/wbctl describe images.md b/doc/wbctl describe images.md index 0389906..8694a01 100644 --- a/doc/wbctl describe images.md +++ b/doc/wbctl describe images.md @@ -1,5 +1,5 @@-Print detailed information about the Image resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Image resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe images [flags] diff --git a/doc/wbctl describe job-submissions.md b/doc/wbctl describe job-submissions.md index 8fa346d..9b38fdb 100644 --- a/doc/wbctl describe job-submissions.md +++ b/doc/wbctl describe job-submissions.md @@ -1,5 +1,5 @@-Print detailed information about the Job-Submission resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Job-Submission resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe job-submissions [flags] diff --git a/doc/wbctl describe jobs.md b/doc/wbctl describe jobs.md index 1e9a544..938d26f 100644 --- a/doc/wbctl describe jobs.md +++ b/doc/wbctl describe jobs.md @@ -1,5 +1,5 @@-Print detailed information about the Job resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Job resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe jobs [flags] diff --git a/doc/wbctl describe snapshots.md b/doc/wbctl describe snapshots.md index d2a2d63..26f35b7 100644 --- a/doc/wbctl describe snapshots.md +++ b/doc/wbctl describe snapshots.md @@ -1,5 +1,5 @@-Print detailed information about the Snapshot resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Snapshot resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe snapshots [flags] diff --git a/doc/wbctl describe volumes.md b/doc/wbctl describe volumes.md index 33aaf4a..e5237a4 100644 --- a/doc/wbctl describe volumes.md +++ b/doc/wbctl describe volumes.md @@ -1,5 +1,5 @@-Print detailed information about the Volume resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Volume resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe volumes [flags] diff --git a/doc/wbctl describe workbenches.md b/doc/wbctl describe workbenches.md index c5a0411..b491c68 100644 --- a/doc/wbctl describe workbenches.md +++ b/doc/wbctl describe workbenches.md @@ -1,5 +1,5 @@-Print detailed information about the Workbench resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print detailed information about the Workbench resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl describe workbenches [flags] diff --git a/doc/wbctl edit image.md b/doc/wbctl edit image.md index d904465..8b863f5 100644 --- a/doc/wbctl edit image.md +++ b/doc/wbctl edit image.md @@ -1,5 +1,5 @@-Edit one Image resource identified by id. One and only one id is required. +Edit one Image resource identified by id or name. One and only one id or name is required. Usage: wbctl edit image [flags] diff --git a/doc/wbctl edit job.md b/doc/wbctl edit job.md index 14e8dc6..d241bf5 100644 --- a/doc/wbctl edit job.md +++ b/doc/wbctl edit job.md @@ -1,5 +1,5 @@-Edit one Job resource identified by id. One and only one id is required. +Edit one Job resource identified by id or name. One and only one id or name is required. Usage: wbctl edit job [flags] diff --git a/doc/wbctl edit snapshot.md b/doc/wbctl edit snapshot.md index 3f9ac81..b037c54 100644 --- a/doc/wbctl edit snapshot.md +++ b/doc/wbctl edit snapshot.md @@ -1,5 +1,5 @@-Edit one Snapshot resource identified by id. One and only one id is required. +Edit one Snapshot resource identified by id or name. One and only one id or name is required. Usage: wbctl edit snapshot [flags] diff --git a/doc/wbctl edit volume.md b/doc/wbctl edit volume.md index 7f4eab1..2a99317 100644 --- a/doc/wbctl edit volume.md +++ b/doc/wbctl edit volume.md @@ -1,5 +1,5 @@-Edit one Volume resource identified by id. One and only one id is required. +Edit one Volume resource identified by id or name. One and only one id or name is required. Usage: wbctl edit volume [flags] diff --git a/doc/wbctl edit workbench.md b/doc/wbctl edit workbench.md index 6ee3a9b..3766394 100644 --- a/doc/wbctl edit workbench.md +++ b/doc/wbctl edit workbench.md @@ -1,5 +1,5 @@-Edit one Workbench resource identified by id. One and only one id is required. +Edit one Workbench resource identified by id or name. One and only one id or name is required. Usage: wbctl edit workbench [flags] diff --git a/doc/wbctl get image-builds.md b/doc/wbctl get image-builds.md index fd389c2..2f71298 100644 --- a/doc/wbctl get image-builds.md +++ b/doc/wbctl get image-builds.md @@ -1,5 +1,5 @@-Print a table of basic information about the Image-Build resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Image-Build resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get image-builds [flags] diff --git a/doc/wbctl get images.md b/doc/wbctl get images.md index a3baec0..515b2b4 100644 --- a/doc/wbctl get images.md +++ b/doc/wbctl get images.md @@ -1,5 +1,5 @@-Print a table of basic information about the Image resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Image resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get images [flags] diff --git a/doc/wbctl get job-submissions.md b/doc/wbctl get job-submissions.md index 0c159a0..c3b0920 100644 --- a/doc/wbctl get job-submissions.md +++ b/doc/wbctl get job-submissions.md @@ -1,5 +1,5 @@-Print a table of basic information about the Job-Submission resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Job-Submission resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get job-submissions [flags] diff --git a/doc/wbctl get jobs.md b/doc/wbctl get jobs.md index 638d897..e057edc 100644 --- a/doc/wbctl get jobs.md +++ b/doc/wbctl get jobs.md @@ -1,5 +1,5 @@-Print a table of basic information about the Job resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Job resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get jobs [flags] diff --git a/doc/wbctl get snapshots.md b/doc/wbctl get snapshots.md index cdf77a6..a7b2df7 100644 --- a/doc/wbctl get snapshots.md +++ b/doc/wbctl get snapshots.md @@ -1,5 +1,5 @@-Print a table of basic information about the Snapshot resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Snapshot resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get snapshots [flags] diff --git a/doc/wbctl get volumes.md b/doc/wbctl get volumes.md index 452178c..534c876 100644 --- a/doc/wbctl get volumes.md +++ b/doc/wbctl get volumes.md @@ -1,5 +1,5 @@-Print a table of basic information about the Volume resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Volume resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get volumes [flags] diff --git a/doc/wbctl get workbenches.md b/doc/wbctl get workbenches.md index c85855d..42d144c 100644 --- a/doc/wbctl get workbenches.md +++ b/doc/wbctl get workbenches.md @@ -1,5 +1,5 @@-Print a table of basic information about the Workbench resources identified by an optional list of resource ids. All resources printed if no ids provided. +Print a table of basic information about the Workbench resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided. Usage: wbctl get workbenches [flags] diff --git a/doc/wbctl logs image-builds.md b/doc/wbctl logs image-builds.md index 5b7c30b..5190eae 100644 --- a/doc/wbctl logs image-builds.md +++ b/doc/wbctl logs image-builds.md @@ -1,5 +1,5 @@-Print the log of the Image-Build resources identified a list of resource ids. At least one id is required. +Print the log of the Image-Build resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl logs image-builds [flags] diff --git a/doc/wbctl logs job-submissions.md b/doc/wbctl logs job-submissions.md index efdae80..a977797 100644 --- a/doc/wbctl logs job-submissions.md +++ b/doc/wbctl logs job-submissions.md @@ -1,5 +1,5 @@-Print the log of the Job-Submission resources identified a list of resource ids. At least one id is required. +Print the log of the Job-Submission resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl logs job-submissions [flags] diff --git a/doc/wbctl rebuild images.md b/doc/wbctl rebuild images.md index 23e839b..cfd74b5 100644 --- a/doc/wbctl rebuild images.md +++ b/doc/wbctl rebuild images.md @@ -1,5 +1,5 @@-Rebuild the Image resources identified by a list of resource ids. At least one id is required. +Rebuild the Image resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl rebuild images [flags] diff --git a/doc/wbctl start jobs.md b/doc/wbctl start jobs.md index 395cbf8..086e201 100644 --- a/doc/wbctl start jobs.md +++ b/doc/wbctl start jobs.md @@ -1,5 +1,5 @@-Start the Job-Submission resources identified by a list of resource ids. At least one id is required. +Start the Job-Submission resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl start jobs [flags] diff --git a/doc/wbctl start workbenches.md b/doc/wbctl start workbenches.md index 695437b..510d296 100644 --- a/doc/wbctl start workbenches.md +++ b/doc/wbctl start workbenches.md @@ -1,5 +1,5 @@-Start the Workbench resources identified by a list of resource ids. At least one id is required. +Start the Workbench resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl start workbenches [flags] diff --git a/doc/wbctl stop job-submission.md b/doc/wbctl stop job-submission.md index 6960c95..b4d0f33 100644 --- a/doc/wbctl stop job-submission.md +++ b/doc/wbctl stop job-submission.md @@ -1,5 +1,5 @@-Cancels the Job-Submission resources identified by a list of resource ids. At least one id is required. +Cancels the Job-Submission resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl stop job-submission [flags] diff --git a/doc/wbctl stop workbenches.md b/doc/wbctl stop workbenches.md index 351f827..0a23abe 100644 --- a/doc/wbctl stop workbenches.md +++ b/doc/wbctl stop workbenches.md @@ -1,5 +1,5 @@-Stop the Workbench resources identified by a list of resource ids. At least one id is required. +Stop the Workbench resources identified by a list of resource ids or names. At least one id or name is required. Usage: wbctl stop workbenches [flags] diff --git a/doc/wbctl update images.md b/doc/wbctl update images.md index e9c198b..d6e54ab 100644 --- a/doc/wbctl update images.md +++ b/doc/wbctl update images.md @@ -1,5 +1,5 @@-Updates all the Image resources identified by a list of ids with the options provided. +Updates all the Image resources identified by a list of ids or names with the options provided. Usage: wbctl update images [flags] diff --git a/doc/wbctl update jobs.md b/doc/wbctl update jobs.md index ba75d2e..27074be 100644 --- a/doc/wbctl update jobs.md +++ b/doc/wbctl update jobs.md @@ -1,5 +1,5 @@-Updates all the Job resources identified by a list of ids with the options provided. +Updates all the Job resources identified by a list of ids or names with the options provided. Usage: wbctl update jobs [flags] diff --git a/doc/wbctl update snapshots.md b/doc/wbctl update snapshots.md index 0247ef4..146a972 100644 --- a/doc/wbctl update snapshots.md +++ b/doc/wbctl update snapshots.md @@ -1,5 +1,5 @@-Updates all the Snapshot resources identified by a list of ids with the options provided. +Updates all the Snapshot resources identified by a list of ids or names with the options provided. Usage: wbctl update snapshots [flags] diff --git a/doc/wbctl update volumes.md b/doc/wbctl update volumes.md index 773668f..1e7b8ea 100644 --- a/doc/wbctl update volumes.md +++ b/doc/wbctl update volumes.md @@ -1,5 +1,5 @@-Updates all the Volume resources identified by a list of ids with the options provided. +Updates all the Volume resources identified by a list of ids or names with the options provided. Usage: wbctl update volumes [flags] diff --git a/doc/wbctl update workbenches.md b/doc/wbctl update workbenches.md index a0e4b5b..0c6f7a5 100644 --- a/doc/wbctl update workbenches.md +++ b/doc/wbctl update workbenches.md @@ -1,5 +1,5 @@-Updates all the Workbench resources identified by a list of ids with the options provided. +Updates all the Workbench resources identified by a list of ids or names with the options provided. Usage: wbctl update workbenches [flags] diff --git a/internal/cmd/cancel/cancel_test.go b/internal/cmd/cancel/cancel_test.go index d97c9fd..4bd2047 100644 --- a/internal/cmd/cancel/cancel_test.go +++ b/internal/cmd/cancel/cancel_test.go @@ -35,7 +35,8 @@ const ( cancelTestControllerID1 = "controller-1" cancelTestControllerID2 = "controller-2" cancelTestGetJobSubmissionsErr = "" + - "GET ${URL}/postgrest/job_runs?controller_id=in.%28controller-1%2Ccontroller-2%29 failed" + + "GET ${URL}/postgrest/job_runs?or=%28controller_id.in.%28controller-1%2Ccontroller-2%29" + + "%2Cjob_name.in.%28controller-1%2Ccontroller-2%29%29 failed" + "\n Caused by:" + "\n\t* unexpected EOF" cancelTestBulkCancelErr = "" + @@ -51,7 +52,8 @@ const ( expectedInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - cancelExpectedGetQuery = "controller_id=in.(controller-1,controller-2)" + cancelExpectedGetQuery = "or=(controller_id.in.(controller-1,controller-2)," + + "job_name.in.(controller-1,controller-2))" ) var cancelExpectedDisplaysStoppedIDs = []string{ diff --git a/internal/cmd/create/image/image_test.go b/internal/cmd/create/image/image_test.go index 0509696..c493c2e 100644 --- a/internal/cmd/create/image/image_test.go +++ b/internal/cmd/create/image/image_test.go @@ -56,6 +56,8 @@ const ( expectedCreateImageServerErr = "POST ${URL}/postgrest/rpc/image_create failed\n" + " Caused by:\n" + "\t* 401 unauthorized" + expectedResolveImagesQueryOne = `or=(controller_id.in.(img-cli-1),name.in.(img-cli-1))` + expectedResolveImagesQueryTwo = `or=(controller_id.in.(img-cli-1,img-cli-2),name.in.(img-cli-1,img-cli-2))` expectedUpdateImageDryRunOutput = `{ "ids": [ "img-cli-1", @@ -120,7 +122,7 @@ const ( expectedInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - expectedInvlidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + + expectedInvalidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" expectedInvalidSpecErr = "invalid character 'i' looking for beginning of value" @@ -374,7 +376,7 @@ func TestCreateImageCommand(t *testing.T) { dataplanesResponseBody: `error`, }), expected: createExpected{ - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, displays: []string{stubs.ExpectedDeviceAuthDisplay}, dataplaneCalls: 1, }, @@ -546,6 +548,8 @@ func TestUpdateImageCommand(t *testing.T) { name: "update success prints IDs only", input: updateInput{args: []string{"img-cli-1"}}, mockEndpoints: getUpdateImageEndpoints( + expectedResolveImagesQueryOne, + `[{"id":"img-cli-1"}]`, expectedUpdateImageRequestBodyIDsOnly, http.StatusOK, `[{"id":"img-a"},{"id":"img-b"}]`, @@ -559,6 +563,8 @@ func TestUpdateImageCommand(t *testing.T) { name: "server error", input: updateInput{args: []string{"img-cli-1"}}, mockEndpoints: getUpdateImageEndpoints( + expectedResolveImagesQueryOne, + `[{"id":"img-cli-1"}]`, expectedUpdateImageRequestBodyIDsOnly, http.StatusUnauthorized, `unauthorized`, @@ -594,6 +600,8 @@ func TestUpdateImageCommand(t *testing.T) { name: "ids only with empty body accepted", input: updateInput{args: []string{"img-cli-1"}}, mockEndpoints: getUpdateImageEndpoints( + expectedResolveImagesQueryOne, + `[{"id":"img-cli-1"}]`, expectedUpdateImageRequestBodyIDsOnly, http.StatusOK, `[{"id":"img-cli-1"}]`, @@ -614,6 +622,8 @@ func TestUpdateImageCommand(t *testing.T) { args: []string{"img-cli-1", "img-cli-2"}, }, mockEndpoints: getUpdateImageEndpoints( + expectedResolveImagesQueryTwo, + `[{"id":"img-cli-1"},{"id":"img-cli-2"}]`, expectedUpdateImageRequestBodyMapping, http.StatusOK, `[{"id":"img-cli-1"},{"id":"img-cli-2"}]`, @@ -636,6 +646,8 @@ func TestUpdateImageCommand(t *testing.T) { args: []string{"img-cli-1"}, }, mockEndpoints: getUpdateImageEndpoints( + expectedResolveImagesQueryOne, + `[{"id":"img-cli-1"}]`, expectedUpdateImageRequestBodyAllFlags, http.StatusOK, `[{"id":"img-cli-1"}]`, @@ -806,12 +818,26 @@ func getUpdateArgs( } func getUpdateImageEndpoints( + expectedQuery string, + resolveResponseBody string, expectedRequestBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { return append(stubs.GetStubAuthEndpoints(), []stubs.StubHTTPEndpoint{ + { + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: postgrest.Endpoint(constants.WorkbenchEndpointImages), + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: expectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: http.StatusOK, + Body: resolveResponseBody, + }, + }, { Request: stubs.StubHTTPRequest{ Method: http.MethodPost, diff --git a/internal/cmd/create/job/job_test.go b/internal/cmd/create/job/job_test.go index 07aea10..252e10a 100644 --- a/internal/cmd/create/job/job_test.go +++ b/internal/cmd/create/job/job_test.go @@ -54,6 +54,8 @@ const ( `"description":"R app","services":["svc-a","svc-b"]}]` expectedServicesResponseBody = `[{"id":"service-1","name":"service 1"},` + `{"id":"service-2","name":"service 2"}]` + expectedResolveImagesQueryOne = `or=(name.in.(job-cli-1))` + expectedResolveImagesQueryTwo = `or=(name.in.(job-cli-1,job-cli-2))` expectedCreateJobRequestBodyWithContext = ` { "param": { @@ -388,13 +390,15 @@ const ( expectedInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - expectedInvlidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + + expectedInvalidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" - expectedInvlidWorkspaceErr = "GET ${URL}/postgrest/workspaces?controller_id=in.%28invalid%29 failed" + + expectedInvalidWorkspaceErr = "GET ${URL}/postgrest/workspaces" + + "?or=%28controller_id.in.%28invalid%29%2Cname.in.%28invalid%29%29 failed" + "\n Caused by:" + "\n\t* 400 error" - expectedInvlidApplicationErr = "GET ${URL}/postgrest/dataplane_workbench_applications?id=in.%28invalid%29 failed" + + expectedInvalidApplicationErr = "GET ${URL}/postgrest/dataplane_workbench_applications" + + "?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" expectedCreateImageInvalidFileErr = "unexpected end of JSON input" @@ -763,7 +767,7 @@ func TestCreateJobCommand(t *testing.T) { }), expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, dataplanesCalls: 1, }, }, @@ -775,7 +779,7 @@ func TestCreateJobCommand(t *testing.T) { }, mockEndpoints: getCreateJobEndpoints(jobEndpointsConfig{ workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(invalid)", + ExpectedQuery: "or=(controller_id.in.(invalid),name.in.(invalid))", ResponseBody: `error`, ResponseStatus: http.StatusBadRequest, }, @@ -783,7 +787,7 @@ func TestCreateJobCommand(t *testing.T) { expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, workspacesCalls: 1, - err: expectedInvlidWorkspaceErr, + err: expectedInvalidWorkspaceErr, }, }, { @@ -802,7 +806,7 @@ func TestCreateJobCommand(t *testing.T) { expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, applicationsCalls: 1, - err: expectedInvlidApplicationErr, + err: expectedInvalidApplicationErr, }, }, } @@ -1033,6 +1037,11 @@ func TestUpdateJobCommand(t *testing.T) { args: []string{"job-cli-1"}, }, mockEndpoints: getUpdateJobEndpoints(jobEndpointsConfig{ + jobsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveImagesQueryOne, + ResponseBody: `[{"id":"job-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, jobConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedUpdateJobRequestBodyIDsOnly, ResponseStatus: http.StatusOK, @@ -1050,6 +1059,11 @@ func TestUpdateJobCommand(t *testing.T) { args: []string{"job-cli-1"}, }, mockEndpoints: getUpdateJobEndpoints(jobEndpointsConfig{ + jobsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveImagesQueryOne, + ResponseBody: `[{"id":"job-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, jobConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedUpdateJobRequestBodyIDsOnly, ResponseStatus: http.StatusUnauthorized, @@ -1096,6 +1110,11 @@ func TestUpdateJobCommand(t *testing.T) { args: []string{"job-cli-1", "job-cli-2"}, }, mockEndpoints: getUpdateJobEndpoints(jobEndpointsConfig{ + jobsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveImagesQueryTwo, + ResponseBody: `[{"id":"job-cli-1"},{"id":"job-cli-2"}]`, + ResponseStatus: http.StatusOK, + }, jobConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedUpdateJobRequestBodyMapping, ResponseStatus: http.StatusOK, @@ -1125,6 +1144,11 @@ func TestUpdateJobCommand(t *testing.T) { args: []string{"job-cli-1"}, }, mockEndpoints: getUpdateJobEndpoints(jobEndpointsConfig{ + jobsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveImagesQueryOne, + ResponseBody: `[{"id":"job-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, jobConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedUpdateJobRequestBodyAllFlags, ResponseStatus: http.StatusOK, @@ -1341,6 +1365,7 @@ type jobEndpointsConfig struct { servicesConfig stubs.StubHTTPPartialGetConfig nodepoolsConfig stubs.StubHTTPPartialGetConfig workspaceConfig stubs.StubHTTPPartialGetConfig + jobsConfig stubs.StubHTTPPartialGetConfig jobConfig stubs.StubHTTPPartialPostConfig } @@ -1353,6 +1378,18 @@ func getJobEndpoints( endpoints = append(endpoints, stubs.GetStubApplicationsEndpoints(config.applicationsConfig)...) endpoints = append(endpoints, stubs.GetStubServicesEndpoints(config.servicesConfig)...) endpoints = append(endpoints, stubs.GetStubNodepoolsEndpoints(config.nodepoolsConfig)...) + endpoints = append(endpoints, stubs.StubHTTPEndpoint{ + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: postgrest.Endpoint(constants.WorkbenchEndpointJobs), + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: config.jobsConfig.ExpectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: config.jobsConfig.ResponseStatus, + Body: config.jobsConfig.ResponseBody, + }, + }) return append(endpoints, []stubs.StubHTTPEndpoint{ { diff --git a/internal/cmd/create/job_submission/job_submission_test.go b/internal/cmd/create/job_submission/job_submission_test.go index ede4a00..3c75d52 100644 --- a/internal/cmd/create/job_submission/job_submission_test.go +++ b/internal/cmd/create/job_submission/job_submission_test.go @@ -44,10 +44,11 @@ const ( expectedCreateJobInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - expectedInvlidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + + expectedInvalidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" - expectedInvlidWorkspaceErr = "GET ${URL}/postgrest/workspaces?controller_id=in.%28invalid%29 failed" + + expectedInvalidWorkspaceErr = "GET ${URL}/postgrest/workspaces" + + "?or=%28controller_id.in.%28invalid%29%2Cname.in.%28invalid%29%29 failed" + "\n Caused by:" + "\n\t* 400 error" ) @@ -236,7 +237,7 @@ func TestCreateJobSubmissionCommand(t *testing.T) { }), expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, dataplaneCalls: 1, }, }, @@ -254,7 +255,7 @@ func TestCreateJobSubmissionCommand(t *testing.T) { ResponseStatus: http.StatusOK, }, workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(invalid)", + ExpectedQuery: "or=(controller_id.in.(invalid),name.in.(invalid))", ResponseStatus: http.StatusBadRequest, ResponseBody: `error`, }, @@ -263,7 +264,7 @@ func TestCreateJobSubmissionCommand(t *testing.T) { displays: []string{stubs.ExpectedDeviceAuthDisplay}, dataplaneCalls: 1, workspaceCalls: 1, - err: expectedInvlidWorkspaceErr, + err: expectedInvalidWorkspaceErr, }, }, } @@ -465,7 +466,7 @@ func TestStartJobCommand(t *testing.T) { }), expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, dataplaneCalls: 1, }, }, @@ -484,7 +485,7 @@ func TestStartJobCommand(t *testing.T) { ResponseStatus: http.StatusOK, }, workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(invalid)", + ExpectedQuery: "or=(controller_id.in.(invalid),name.in.(invalid))", ResponseStatus: http.StatusBadRequest, ResponseBody: `error`, }, @@ -493,7 +494,7 @@ func TestStartJobCommand(t *testing.T) { displays: []string{stubs.ExpectedDeviceAuthDisplay}, dataplaneCalls: 1, workspaceCalls: 1, - err: expectedInvlidWorkspaceErr, + err: expectedInvalidWorkspaceErr, }, }, } diff --git a/internal/cmd/create/snapshot/snapshot_test.go b/internal/cmd/create/snapshot/snapshot_test.go index bc765c8..8a7a1ec 100644 --- a/internal/cmd/create/snapshot/snapshot_test.go +++ b/internal/cmd/create/snapshot/snapshot_test.go @@ -65,8 +65,9 @@ const ( } } }` - snapshotUpdateMissingIDErr = "at least one resource ID must be provided for update" - snapshotUpdateServerErr = "POST ${URL}/postgrest/rpc/snapshot_bulk_update failed\n" + + expectedResolveSnapshotsQuery = `or=(controller_id.in.(snap-cli-1),name.in.(snap-cli-1))` + snapshotUpdateMissingIDErr = "at least one resource ID must be provided for update" + snapshotUpdateServerErr = "POST ${URL}/postgrest/rpc/snapshot_bulk_update failed\n" + " Caused by:\n" + "\t* 401 unauthorized" snapshotUpdateLongFlagsDryRunOutput = `{ @@ -86,11 +87,12 @@ const ( expectedCreateSnapshotInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - expectedInvlidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + + expectedInvalidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" expectedInvalidVolumeErr = "" + - "failed to get volume invalid: GET ${URL}/postgrest/workspaces?controller_id=in.%28invalid%29 failed" + + "failed to get volume invalid: GET ${URL}/postgrest/workspaces" + + "?or=%28controller_id.in.%28invalid%29%2Cname.in.%28invalid%29%29 failed" + "\n Caused by:" + "\n\t* 400 error" expectedCreateImageInvalidFileErr = "unexpected end of JSON input" ) @@ -157,7 +159,7 @@ func TestCreateSnapshotCommand(t *testing.T) { ResponseStatus: http.StatusOK, }, workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(ws-1)`, + ExpectedQuery: `or=(controller_id.in.(ws-1),name.in.(ws-1))`, ResponseBody: `[{"id":"ws-1","controller_id":"ctrl-1"}]`, ResponseStatus: http.StatusOK, }, @@ -189,7 +191,7 @@ func TestCreateSnapshotCommand(t *testing.T) { ResponseStatus: http.StatusOK, }, workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(ws-1)`, + ExpectedQuery: `or=(controller_id.in.(ws-1),name.in.(ws-1))`, ResponseBody: `[{"id":"ws-1","controller_id":"ctrl-1"}]`, ResponseStatus: http.StatusOK, }, @@ -220,7 +222,7 @@ func TestCreateSnapshotCommand(t *testing.T) { ResponseStatus: http.StatusOK, }, workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(ws-1)`, + ExpectedQuery: `or=(controller_id.in.(ws-1),name.in.(ws-1))`, ResponseBody: `[{"id":"ws-1","controller_id":"ctrl-1"}]`, ResponseStatus: http.StatusOK, }, @@ -259,7 +261,7 @@ func TestCreateSnapshotCommand(t *testing.T) { }, mockEndpoints: getCreateSnapshotEndpoints(snapshotEndpointsConfig{ workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(missing)`, + ExpectedQuery: `or=(controller_id.in.(missing),name.in.(missing))`, ResponseBody: `[]`, ResponseStatus: http.StatusOK, }, @@ -280,7 +282,7 @@ func TestCreateSnapshotCommand(t *testing.T) { }, mockEndpoints: getCreateSnapshotEndpoints(snapshotEndpointsConfig{ workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(invalid)`, + ExpectedQuery: `or=(controller_id.in.(invalid),name.in.(invalid))`, ResponseBody: `error`, ResponseStatus: http.StatusBadRequest, }, @@ -315,7 +317,7 @@ func TestCreateSnapshotCommand(t *testing.T) { }), expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, dataplaneCalls: 1, }, }, @@ -443,7 +445,7 @@ func TestUpdateSnapshotCommand(t *testing.T) { }, mockEndpoints: getUpdateSnapshotEndpoints(snapshotEndpointsConfig{ workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(ws-1)`, + ExpectedQuery: `or=(controller_id.in.(ws-1),name.in.(ws-1))`, ResponseBody: `[{"id":"ws-1","controller_id":"ctrl-1"}]`, ResponseStatus: http.StatusOK, }, @@ -471,6 +473,11 @@ func TestUpdateSnapshotCommand(t *testing.T) { args: []string{"snap-cli-1"}, }, mockEndpoints: getUpdateSnapshotEndpoints(snapshotEndpointsConfig{ + snapshotsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveSnapshotsQuery, + ResponseBody: `[{"id":"snap-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, snapshotConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: snapshotUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusOK, @@ -488,6 +495,11 @@ func TestUpdateSnapshotCommand(t *testing.T) { args: []string{"snap-cli-1"}, }, mockEndpoints: getUpdateSnapshotEndpoints(snapshotEndpointsConfig{ + snapshotsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveSnapshotsQuery, + ResponseBody: `[{"id":"snap-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, snapshotConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: snapshotUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusUnauthorized, @@ -529,6 +541,11 @@ func TestUpdateSnapshotCommand(t *testing.T) { args: []string{"snap-cli-1"}, }, mockEndpoints: getUpdateSnapshotEndpoints(snapshotEndpointsConfig{ + snapshotsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveSnapshotsQuery, + ResponseBody: `[{"id":"snap-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, snapshotConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: snapshotUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusOK, @@ -550,10 +567,15 @@ func TestUpdateSnapshotCommand(t *testing.T) { }, mockEndpoints: getUpdateSnapshotEndpoints(snapshotEndpointsConfig{ workspaceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(ws-1)`, + ExpectedQuery: `or=(controller_id.in.(ws-1),name.in.(ws-1))`, ResponseBody: `[{"id":"ws-1","controller_id":"ctrl-1"}]`, ResponseStatus: http.StatusOK, }, + snapshotsConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveSnapshotsQuery, + ResponseBody: `[{"id":"snap-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, snapshotConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: snapshotUpdateRequestBodyMapping, ResponseStatus: http.StatusOK, @@ -703,6 +725,7 @@ func getUpdateArgs( type snapshotEndpointsConfig struct { dataplaneConfig stubs.StubHTTPPartialGetConfig workspaceConfig stubs.StubHTTPPartialGetConfig + snapshotsConfig stubs.StubHTTPPartialGetConfig snapshotConfig stubs.StubHTTPPartialPostConfig } @@ -712,6 +735,18 @@ func getSnapshotEndpoints( ) []stubs.StubHTTPEndpoint { endpoints := append(stubs.GetStubAuthEndpoints(), stubs.GetStubWorkspaceEndpoints(config.workspaceConfig)...) endpoints = append(endpoints, stubs.GetStubDataplaneEndpoints(config.dataplaneConfig)...) + endpoints = append(endpoints, stubs.StubHTTPEndpoint{ + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: postgrest.Endpoint(constants.WorkbenchEndpointSnapshots), + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: config.snapshotsConfig.ExpectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: config.snapshotsConfig.ResponseStatus, + Body: config.snapshotsConfig.ResponseBody, + }, + }) return append(endpoints, []stubs.StubHTTPEndpoint{ { diff --git a/internal/cmd/create/workbench/workbench_test.go b/internal/cmd/create/workbench/workbench_test.go index b0cc081..94d4ab4 100644 --- a/internal/cmd/create/workbench/workbench_test.go +++ b/internal/cmd/create/workbench/workbench_test.go @@ -112,20 +112,24 @@ const ( expectedCreateSnapshotInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - expectedInvlidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + + expectedInvalidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" - expectedInvlidWorkspaceErr = "GET ${URL}/postgrest/workspaces?controller_id=in.%28invalid%29 failed" + + expectedInvalidWorkspaceErr = "GET ${URL}/postgrest/workspaces" + + "?or=%28controller_id.in.%28invalid%29%2Cname.in.%28invalid%29%29 failed" + "\n Caused by:" + "\n\t* 400 error" - expectedInvlidApplicationErr = "GET ${URL}/postgrest/dataplane_workbench_applications?id=in.%28invalid%29 failed" + + expectedInvalidApplicationErr = "GET ${URL}/postgrest/dataplane_workbench_applications" + + "?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" expectedApplicationQuery = "id=in.(app-1)" applicationResponseBody = `[{"id":"app-1","services":["svc-a","svc-b"]}]` servicesResponseBody = `[{"id":"service-1","name":"service 1"},` + `{"id":"service-2","name":"service 2"}]` - expectedCreateImageInvalidFileErr = "unexpected end of JSON input" + expectedResolveWorkbenchesQueryOne = `or=(controller_id.in.(wb-cli-1),name.in.(wb-cli-1))` + expectedResolveWorkbenchesQueryTwo = `or=(controller_id.in.(wb-cli-1,wb-cli-2),name.in.(wb-cli-1,wb-cli-2))` + expectedCreateImageInvalidFileErr = "unexpected end of JSON input" ) type createInput struct { @@ -454,7 +458,7 @@ func TestCreateWorkbenchCommand(t *testing.T) { }), expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, dataplaneCalls: 1, }, }, @@ -466,7 +470,7 @@ func TestCreateWorkbenchCommand(t *testing.T) { }, mockEndpoints: getCreateWorkbenchEndpoints(workbenchEndpointsConfig{ workspacesConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(invalid)`, + ExpectedQuery: `or=(controller_id.in.(invalid),name.in.(invalid))`, ResponseStatus: http.StatusBadRequest, ResponseBody: `error`, }, @@ -474,7 +478,7 @@ func TestCreateWorkbenchCommand(t *testing.T) { expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, workspaceCalls: 1, - err: expectedInvlidWorkspaceErr, + err: expectedInvalidWorkspaceErr, }, }, { @@ -493,7 +497,7 @@ func TestCreateWorkbenchCommand(t *testing.T) { expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, applicationsCalls: 1, - err: expectedInvlidApplicationErr, + err: expectedInvalidApplicationErr, }, }, } @@ -733,6 +737,11 @@ func TestUpdateWorkbenchCommand(t *testing.T) { args: []string{"wb-cli-1"}, }, mockEndpoints: getBulkUpdateWorkbenchEndpoints(workbenchEndpointsConfig{ + workbenchesResolveConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkbenchesQueryOne, + ResponseBody: `[{"id":"wb-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workbenchesConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workbenchUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusUnauthorized, @@ -774,6 +783,11 @@ func TestUpdateWorkbenchCommand(t *testing.T) { args: []string{"wb-cli-1"}, }, mockEndpoints: getBulkUpdateWorkbenchEndpoints(workbenchEndpointsConfig{ + workbenchesResolveConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkbenchesQueryOne, + ResponseBody: `[{"id":"wb-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workbenchesConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workbenchUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusOK, @@ -799,6 +813,11 @@ func TestUpdateWorkbenchCommand(t *testing.T) { ResponseStatus: http.StatusOK, ResponseBody: applicationResponseBody, }, + workbenchesResolveConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkbenchesQueryTwo, + ResponseBody: `[{"id":"wb-cli-1"},{"id":"wb-cli-2"}]`, + ResponseStatus: http.StatusOK, + }, workbenchesConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workbenchUpdateRequestBodyMapping, ResponseStatus: http.StatusOK, @@ -818,6 +837,11 @@ func TestUpdateWorkbenchCommand(t *testing.T) { args: []string{"wb-cli-1"}, }, mockEndpoints: getBulkUpdateWorkbenchEndpoints(workbenchEndpointsConfig{ + workbenchesResolveConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkbenchesQueryOne, + ResponseBody: `[{"id":"wb-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workbenchesConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workbenchUpdateRequestBodyWorkspace, ResponseStatus: http.StatusOK, @@ -838,6 +862,11 @@ func TestUpdateWorkbenchCommand(t *testing.T) { args: []string{"wb-cli-1"}, }, mockEndpoints: getUpdateWorkbenchEndpoints(workbenchEndpointsConfig{ + workbenchesResolveConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkbenchesQueryOne, + ResponseBody: `[{"id":"wb-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workbenchesConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workbenchUpdateRequestBodyNameLabels, ResponseStatus: http.StatusOK, @@ -1030,12 +1059,13 @@ func getUpdateArgs( } type workbenchEndpointsConfig struct { - dataplaneConfig stubs.StubHTTPPartialGetConfig - applicationsConfig stubs.StubHTTPPartialGetConfig - servicesConfig stubs.StubHTTPPartialGetConfig - nodepoolsConfig stubs.StubHTTPPartialGetConfig - workspacesConfig stubs.StubHTTPPartialGetConfig - workbenchesConfig stubs.StubHTTPPartialPostConfig + dataplaneConfig stubs.StubHTTPPartialGetConfig + applicationsConfig stubs.StubHTTPPartialGetConfig + servicesConfig stubs.StubHTTPPartialGetConfig + nodepoolsConfig stubs.StubHTTPPartialGetConfig + workspacesConfig stubs.StubHTTPPartialGetConfig + workbenchesResolveConfig stubs.StubHTTPPartialGetConfig + workbenchesConfig stubs.StubHTTPPartialPostConfig } func getWorkbenchEndpoints( @@ -1047,6 +1077,18 @@ func getWorkbenchEndpoints( endpoints = append(endpoints, stubs.GetStubApplicationsEndpoints(config.applicationsConfig)...) endpoints = append(endpoints, stubs.GetStubServicesEndpoints(config.servicesConfig)...) endpoints = append(endpoints, stubs.GetStubNodepoolsEndpoints(config.nodepoolsConfig)...) + endpoints = append(endpoints, stubs.StubHTTPEndpoint{ + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: config.workbenchesResolveConfig.ExpectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: config.workbenchesResolveConfig.ResponseStatus, + Body: config.workbenchesResolveConfig.ResponseBody, + }, + }) return append(endpoints, []stubs.StubHTTPEndpoint{ { @@ -1071,6 +1113,7 @@ func getCreateWorkbenchEndpoints(config workbenchEndpointsConfig) []stubs.StubHT func getUpdateWorkbenchEndpoints(config workbenchEndpointsConfig) []stubs.StubHTTPEndpoint { return getWorkbenchEndpoints(postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchUpdate), config) } + func getBulkUpdateWorkbenchEndpoints(config workbenchEndpointsConfig) []stubs.StubHTTPEndpoint { return getWorkbenchEndpoints(postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchBulkUpdate), config) } diff --git a/internal/cmd/create/workspace/workspace_test.go b/internal/cmd/create/workspace/workspace_test.go index e2fe87c..128ef8c 100644 --- a/internal/cmd/create/workspace/workspace_test.go +++ b/internal/cmd/create/workspace/workspace_test.go @@ -86,6 +86,7 @@ const ( } } }` + expectedResolveWorkspacesQueryOne = `or=(controller_id.in.(ws-cli-1),name.in.(ws-cli-1))` workspaceUpdateRequestBodyIDsOnly = `{"ids":["ws-cli-1"],"workspace":{"pvc_status":{"status":{}}}}` workspaceUpdateFileContent = `{"id":"ws-file","description":"from file"}` workspaceUpdateFileDryRunOutput = `{ @@ -114,7 +115,7 @@ const ( expectedCreateSnapshotInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" - expectedInvlidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + + expectedInvalidDataplaneErr = "GET ${URL}/postgrest/dataplanes?id=in.%28invalid%29 failed" + "\n Caused by:" + "\n\t* 400 error" expectedInvalidInitalizersErr = "" + @@ -128,7 +129,8 @@ const ( "\n Caused by:" + "\n\t* 400 error" expectedInvalidSnapshotErr = "" + - "GET ${URL}/postgrest/snapshots?controller_id=in.%28snap-1%29 failed" + + "GET ${URL}/postgrest/snapshots?" + + "or=%28controller_id.in.%28snap-1%29%2Cname.in.%28snap-1%29%29 failed" + "\n Caused by:" + "\n\t* invalid character 'i' looking for beginning of value" ) @@ -271,7 +273,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { ResponseStatus: http.StatusOK, }, snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-1)`, + ExpectedQuery: `or=(controller_id.in.(snap-1),name.in.(snap-1))`, ResponseStatus: http.StatusOK, ResponseBody: workspaceSnapshotLookupBody, }, @@ -403,7 +405,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { }, mockEndpoints: getCreateWorkspaceEndpoints(workspaceEndpointsConfig{ snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(invalid-snap)`, + ExpectedQuery: `or=(controller_id.in.(invalid-snap),name.in.(invalid-snap))`, ResponseBody: `[]`, ResponseStatus: http.StatusOK, }, @@ -423,7 +425,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { }, mockEndpoints: getCreateWorkspaceEndpoints(workspaceEndpointsConfig{ snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-not-ready)`, + ExpectedQuery: `or=(controller_id.in.(snap-not-ready),name.in.(snap-not-ready))`, ResponseBody: `[{"id":"snap-not-ready","controller_id":"","volume_status":{"status":` + `{"readyToUse":false}}}]`, ResponseStatus: http.StatusOK, @@ -445,7 +447,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { }, mockEndpoints: getCreateWorkspaceEndpoints(workspaceEndpointsConfig{ snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-no-size)`, + ExpectedQuery: `or=(controller_id.in.(snap-no-size),name.in.(snap-no-size))`, ResponseBody: `[{"id":"snap-no-size","controller_id":"ctrl-snap","workspace_source":{` + `"storage_spec":{"accessModes":["ReadWriteOnce"],"storageClassName":"snap-sc"}},` + `"volume_status":{"status":{"readyToUse":true}}}]`, @@ -468,7 +470,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { }, mockEndpoints: getCreateWorkspaceEndpoints(workspaceEndpointsConfig{ snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-1)`, + ExpectedQuery: `or=(controller_id.in.(snap-1),name.in.(snap-1))`, ResponseBody: `[{"id":"snap-1","controller_id":"ctrl-snap","volume_status":` + `{"status":{"readyToUse":true}}}]`, ResponseStatus: http.StatusOK, @@ -490,7 +492,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { }, mockEndpoints: getCreateWorkspaceEndpoints(workspaceEndpointsConfig{ snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-1)`, + ExpectedQuery: `or=(controller_id.in.(snap-1),name.in.(snap-1))`, ResponseBody: `[{"id":"snap-1","controller_id":"ctrl-snap","workspace_source":{"storage_spec":` + `{"accessModes":[],"storageClassName":"snap-sc"}},"volume_status":{"status":` + `{"readyToUse":true,"restoreSize":"1Gi"}}}]`, @@ -528,14 +530,14 @@ func TestCreateWorkspaceCommand(t *testing.T) { ResponseBody: `error`, }, snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-1)`, + ExpectedQuery: `or=(controller_id.in.(snap-1),name.in.(snap-1))`, ResponseBody: workspaceSnapshotLookupBody, ResponseStatus: http.StatusOK, }, }), expected: createExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: expectedInvlidDataplaneErr, + err: expectedInvalidDataplaneErr, dataplaneCalls: 1, snapshotCalls: 1, }, @@ -591,7 +593,7 @@ func TestCreateWorkspaceCommand(t *testing.T) { }, mockEndpoints: getCreateWorkspaceEndpoints(workspaceEndpointsConfig{ snapshotsConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: `controller_id=in.(snap-1)`, + ExpectedQuery: `or=(controller_id.in.(snap-1),name.in.(snap-1))`, ResponseStatus: http.StatusOK, ResponseBody: `[invalid`, }, @@ -771,6 +773,11 @@ func TestUpdateWorkspaceCommand(t *testing.T) { args: []string{"ws-cli-1"}, }, mockEndpoints: getBulkUpdateWorkspaceEndpoints(workspaceUpdateEndpointsConfig{ + workspacesConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkspacesQueryOne, + ResponseBody: `[{"id":"ws-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workspaceConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workspaceUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusOK, @@ -788,6 +795,11 @@ func TestUpdateWorkspaceCommand(t *testing.T) { args: []string{"ws-cli-1"}, }, mockEndpoints: getBulkUpdateWorkspaceEndpoints(workspaceUpdateEndpointsConfig{ + workspacesConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkspacesQueryOne, + ResponseBody: `[{"id":"ws-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workspaceConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workspaceUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusUnauthorized, @@ -829,6 +841,11 @@ func TestUpdateWorkspaceCommand(t *testing.T) { args: []string{"ws-cli-1"}, }, mockEndpoints: getBulkUpdateWorkspaceEndpoints(workspaceUpdateEndpointsConfig{ + workspacesConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkspacesQueryOne, + ResponseBody: `[{"id":"ws-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workspaceConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workspaceUpdateRequestBodyIDsOnly, ResponseStatus: http.StatusOK, @@ -848,6 +865,11 @@ func TestUpdateWorkspaceCommand(t *testing.T) { args: []string{"ws-cli-1"}, }, mockEndpoints: getSingleUpdateWorkspaceEndpoints(workspaceUpdateEndpointsConfig{ + workspacesConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedResolveWorkspacesQueryOne, + ResponseBody: `[{"id":"ws-cli-1"}]`, + ResponseStatus: http.StatusOK, + }, workspaceConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: workspaceUpdateRequestBodyNameDesc, ResponseStatus: http.StatusOK, @@ -1045,6 +1067,7 @@ type workspaceEndpointsConfig struct { type workspaceUpdateEndpointsConfig struct { initializerConfig stubs.StubHTTPPartialGetConfig + workspacesConfig stubs.StubHTTPPartialGetConfig workspaceConfig stubs.StubHTTPPartialPostConfig } @@ -1126,6 +1149,18 @@ func getUpdateWorkspaceEndpoints( ) []stubs.StubHTTPEndpoint { endpoints := append(stubs.GetStubAuthEndpoints(), getInitializerEndpoints(config.initializerConfig)...) + endpoints = append(endpoints, stubs.StubHTTPEndpoint{ + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: config.workspacesConfig.ExpectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: config.workspacesConfig.ResponseStatus, + Body: config.workspacesConfig.ResponseBody, + }, + }) return append(endpoints, stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, diff --git a/internal/cmd/delete/delete_test.go b/internal/cmd/delete/delete_test.go index 0a10fd4..fda2d92 100644 --- a/internal/cmd/delete/delete_test.go +++ b/internal/cmd/delete/delete_test.go @@ -38,6 +38,24 @@ const ( "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" ) +const ( + expectedImageResolveQuery = `or=(controller_id.in.(image-1,image-2),name.in.(image-1,image-2))` + expectedJobResolveQuery = `or=(name.in.(job-1,job-2))` + expectedSnapshotResolveQuery = `or=(controller_id.in.(snapshot-1,snapshot-2),name.in.(snapshot-1,snapshot-2))` + expectedSubmissionResolveQuery = `or=(controller_id.in.(submission-1,submission-2),` + + `job_name.in.(submission-1,submission-2))` + expectedWorkbenchResolveQuery = `or=(controller_id.in.(workbench-1,workbench-2),name.in.(workbench-1,workbench-2))` + expectedWorkspaceResolveQueryOne = `or=(controller_id.in.(workspace-1),name.in.(workspace-1))` + expectedWorkspaceResolveQueryTwo = `or=(controller_id.in.(workspace-1,workspace-2),name.in.(workspace-1,workspace-2))` + imageResolveResponseBody = `[{"id":"image-1"},{"id":"image-2"}]` + jobResolveResponseBody = `[{"id":"job-1"},{"id":"job-2"}]` + snapshotResolveResponseBody = `[{"id":"snapshot-1"},{"id":"snapshot-2"}]` + submissionResolveResponseBody = `[{"id":"submission-1"},{"id":"submission-2"}]` + workbenchResolveResponseBody = `[{"id":"workbench-1"},{"id":"workbench-2"}]` + workspaceResolveResponseBodyOne = `[{"id":"workspace-1"}]` + workspaceResolveResponseBodyTwo = `[{"id":"workspace-1"},{"id":"workspace-2"}]` +) + var ( expectedImageDisplay = strings.Join([]string{image1ID, image2ID}, "\n") expectedJobDisplay = strings.Join([]string{job1ID, job2ID}, "\n") @@ -99,6 +117,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{workspace1ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedWorkspaceResolveQueryOne, + ResponseStatus: http.StatusOK, + ResponseBody: workspaceResolveResponseBodyOne, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkspaceBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: `{"ids":["workspace-1"]}`, @@ -121,6 +145,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{image1ID, image2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointImages), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedImageResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: imageResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCImageBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedImageRequestBody, @@ -140,6 +170,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{image1ID, image2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointImages), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedImageResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: imageResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCImageBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedImageRequestBody, @@ -159,6 +195,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{submission1ID, submission2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedSubmissionResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: submissionResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCJobRunBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedSubmissionRequestBody, @@ -178,6 +220,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{submission1ID, submission2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedSubmissionResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: submissionResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCJobRunBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedSubmissionRequestBody, @@ -197,6 +245,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{job1ID, job2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointJobs), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedJobResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: jobResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCJobBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedJobRequestBody, @@ -216,6 +270,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{job1ID, job2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointJobs), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedJobResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: jobResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCJobBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedJobRequestBody, @@ -235,6 +295,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{snapshot1ID, snapshot2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointSnapshots), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedSnapshotResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: snapshotResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCSnapshotBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedSnapshotRequestBody, @@ -254,6 +320,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{snapshot1ID, snapshot2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointSnapshots), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedSnapshotResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: snapshotResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCSnapshotBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedSnapshotRequestBody, @@ -273,6 +345,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{workbench1ID, workbench2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedWorkbenchResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: workbenchResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedWorkbenchRequestBody, @@ -292,6 +370,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{workbench1ID, workbench2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedWorkbenchResolveQuery, + ResponseStatus: http.StatusOK, + ResponseBody: workbenchResolveResponseBody, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedWorkbenchRequestBody, @@ -311,6 +395,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{workspace1ID, workspace2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedWorkspaceResolveQueryTwo, + ResponseStatus: http.StatusOK, + ResponseBody: workspaceResolveResponseBodyTwo, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkspaceBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedWorkspaceRequestBody, @@ -330,6 +420,12 @@ func TestDeleteCommand(t *testing.T) { resourceIDs: []string{workspace1ID, workspace2ID}, }, mockEndpoints: getDeleteEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), + stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: expectedWorkspaceResolveQueryTwo, + ResponseStatus: http.StatusOK, + ResponseBody: workspaceResolveResponseBodyTwo, + }, postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkspaceBulkDelete), stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: expectedWorkspaceRequestBody, @@ -432,10 +528,23 @@ func getDeleteArgs( } func getDeleteEndpoints( + resolveEndpoint string, + resolveConfig stubs.StubHTTPPartialGetConfig, urlUnderTest string, config ...stubs.StubHTTPPartialPostConfig, ) []stubs.StubHTTPEndpoint { - endpoints := stubs.GetStubAuthEndpoints() + endpoints := append(stubs.GetStubAuthEndpoints(), stubs.StubHTTPEndpoint{ + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: resolveEndpoint, + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: resolveConfig.ExpectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: resolveConfig.ResponseStatus, + Body: resolveConfig.ResponseBody, + }, + }) for _, cfg := range config { endpoints = append(endpoints, stubs.StubHTTPEndpoint{ diff --git a/internal/cmd/describe/command.go b/internal/cmd/describe/command.go index 6e8fb7d..a905d5d 100644 --- a/internal/cmd/describe/command.go +++ b/internal/cmd/describe/command.go @@ -61,7 +61,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageType)}, ), }, @@ -75,7 +75,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageBuildType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageBuildType)}, ), }, @@ -89,7 +89,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobType)}, ), }, @@ -103,7 +103,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobSubmissionType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobSubmissionType)}, ), }, @@ -117,7 +117,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.SnapshotType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.SnapshotType)}, ), }, @@ -145,7 +145,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.WorkbenchType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.WorkbenchType)}, ), }, @@ -181,7 +181,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.VolumeType)}, ), Long: i18n.Format( - i18n.CmdDescribeTypeDescription, + i18n.CmdDescribeTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.VolumeType)}, ), }, diff --git a/internal/cmd/describe/describe_test.go b/internal/cmd/describe/describe_test.go index f9fb455..98cb88b 100644 --- a/internal/cmd/describe/describe_test.go +++ b/internal/cmd/describe/describe_test.go @@ -589,7 +589,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, @@ -608,7 +608,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointImages), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, @@ -627,7 +627,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),job_name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, @@ -646,7 +646,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointJobs), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "id=in.(resource-1)", + ExpectedQuery: "or=(name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, @@ -665,7 +665,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointSnapshots), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, @@ -703,7 +703,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, @@ -722,7 +722,7 @@ func TestDescribeCommand(t *testing.T) { mockEndpoints: getDescribeEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: describeGenericResourceBySingularTypeExpectedResponseBody, }, diff --git a/internal/cmd/edit/edit_test.go b/internal/cmd/edit/edit_test.go index 220e455..8349f55 100644 --- a/internal/cmd/edit/edit_test.go +++ b/internal/cmd/edit/edit_test.go @@ -27,6 +27,7 @@ const ( testResourceID1 = "resource-1" oneImageJSON = `[ { + "id": "resource-1", "image_name": "test-image", "image_registry": "example.com", "image_tag": "latest", @@ -44,11 +45,20 @@ const ( "image_name": "test-image", "image_registry": "example.com", "image_tag": "latest", - "name": "resource-2" + "name": "resource-1" } ]` oneUpdatedImageJSON = `{"ids":["resource-1"],"param":{"image_name":"test-image",` + `"image_registry":"example.com","image_tag":"latest","name":"UPDATED"}}` + oneUpdatedImageResponseJSON = `[ + { + "id": "resource-1", + "image_name": "test-image", + "image_registry": "example.com", + "image_tag": "latest", + "name": "UPDATED" + } +]` oneWorkbenchJSON = `[ { "id": "resource-1", @@ -129,7 +139,7 @@ const ( "workspaces": [ { "id": "sas-workbench-v0fq4c4ff9xqlv70j76amcqe", - "mountName": "replace-me" + "mountName": "UPDATED" } ] }` @@ -145,9 +155,10 @@ const ( `"workspaces":[{"id":"sas-workbench-v0fq4c4ff9xqlv70j76amcqe","mountName":"UPDATED"}]` + `}}` oneJobJSON = `[{ + "id": "resource-1", "application_id": "workbench-environment-builder", "dataplane_id": "ae0d1d17-c239-443e-ab80-70731b9e15de", - "name": "Job-2", + "name": "resource-1", "nodepool": "C0M0", "services": [ { @@ -178,17 +189,49 @@ const ( oneUpdatedJobJSON = `{"ids":["resource-1"],` + `"param":{"application_id":"workbench-environment-builder",` + `"dataplane_id":"ae0d1d17-c239-443e-ab80-70731b9e15de",` + - `"name":"Job-2",` + + `"name":"resource-1",` + `"nodepool":"C0M0",` + `"services":[{"id":"workbench-environment-builder",` + `"env":{"DEFAULT_MOUNTNAME":"containers","WB_LOCALE":"en-US,en"}}],` + `"spec":{"tasks":{"default":{"cmds":["echo \"test123\""]}},"version":"3"},` + `"workspaces":[{"id":"sas-workbench-5tcc2a32w0wu293go4nu8ldvj","mountName":"UPDATED"}]}}` + oneUpdatedJobResponseJSON = `[{ + "id": "resource-1", + "application_id": "workbench-environment-builder", + "dataplane_id": "ae0d1d17-c239-443e-ab80-70731b9e15de", + "name": "resource-1", + "nodepool": "C0M0", + "services": [ + { + "id": "workbench-environment-builder", + "env": { + "DEFAULT_MOUNTNAME": "containers", + "WB_LOCALE": "en-US,en" + } + } + ], + "spec": { + "tasks": { + "default": { + "cmds": [ + "echo \"test123\"" + ] + } + }, + "version": "3" + }, + "workspaces": [ + { + "id": "sas-workbench-5tcc2a32w0wu293go4nu8ldvj", + "mountName": "UPDATED" + } + ] +}]` expectedInvalidEndpointErr = "failed to get device token" + "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" expectedInvalidResourceErr = "" + - "GET ${URL}/postgrest/jobs?id=in.%28resource-1%29 failed" + + "GET ${URL}/postgrest/jobs?or=%28name.in.%28resource-1%29%29 failed" + "\n Caused by:" + "\n\t* invalid character 'i' looking for beginning of value" expectedEditErr = "" + @@ -275,7 +318,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: "[]", }, @@ -292,7 +335,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: twoImagesJSON, }, @@ -315,7 +358,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -338,7 +381,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -362,7 +405,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -389,7 +432,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -410,7 +453,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -433,7 +476,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -456,7 +499,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -464,11 +507,11 @@ func TestEditCommand(t *testing.T) { postConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: oneUpdatedImageJSON, ResponseStatus: http.StatusOK, - ResponseBody: oneImageJSON, + ResponseBody: oneUpdatedImageResponseJSON, }, }), expected: editExpected{ - getCalls: 1, + getCalls: 2, postCalls: 1, displays: []string{stubs.ExpectedDeviceAuthDisplay, "resource-1"}, }, @@ -483,7 +526,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -510,7 +553,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointImages), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneImageJSON, }, @@ -531,7 +574,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(resource-1)", + ExpectedQuery: "or=(controller_id.in.(resource-1),name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneWorkbenchJSON, }, @@ -543,7 +586,7 @@ func TestEditCommand(t *testing.T) { }, }), expected: editExpected{ - getCalls: 1, + getCalls: 2, postCalls: 1, displays: []string{stubs.ExpectedDeviceAuthDisplay, "resource-1"}, }, @@ -558,7 +601,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointJobs), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "id=in.(resource-1)", + ExpectedQuery: "or=(name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneJobJSON, }, @@ -566,11 +609,11 @@ func TestEditCommand(t *testing.T) { postConfig: stubs.StubHTTPPartialPostConfig{ ExpectedRequestBody: oneUpdatedJobJSON, ResponseStatus: http.StatusOK, - ResponseBody: oneJobJSON, + ResponseBody: oneUpdatedJobResponseJSON, }, }), expected: editExpected{ - getCalls: 1, + getCalls: 2, postCalls: 1, displays: []string{stubs.ExpectedDeviceAuthDisplay, "resource-1"}, }, @@ -595,7 +638,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointJobs), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "id=in.(resource-1)", + ExpectedQuery: "or=(name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: `[invalid`, }, @@ -616,7 +659,7 @@ func TestEditCommand(t *testing.T) { mockEndpoints: getEditEndpoints(editConfig{ getURL: postgrest.Endpoint(constants.WorkbenchEndpointJobs), getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "id=in.(resource-1)", + ExpectedQuery: "or=(name.in.(resource-1))", ResponseStatus: http.StatusOK, ResponseBody: oneJobJSON, }, @@ -629,7 +672,7 @@ func TestEditCommand(t *testing.T) { }), expected: editExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - getCalls: 1, + getCalls: 2, postCalls: 1, err: expectedEditErr, }, @@ -734,20 +777,23 @@ type editConfig struct { func getEditEndpoints( config editConfig, ) []stubs.StubHTTPEndpoint { + getEndpoint := stubs.StubHTTPEndpoint{ + Request: stubs.StubHTTPRequest{ + Method: http.MethodGet, + URL: config.getURL, + ExpectedHeaders: stubs.StubAuthHeaders, + ExpectedQuery: config.getConfig.ExpectedQuery, + }, + Response: stubs.StubHTTPResponse{ + Status: config.getConfig.ResponseStatus, + Body: config.getConfig.ResponseBody, + }, + } endpoints := stubs.GetStubAuthEndpoints() + endpoints = append(endpoints, getEndpoint) + // ID resolution repeats the GET before bulk updates. + endpoints = append(endpoints, getEndpoint) endpoints = append(endpoints, - stubs.StubHTTPEndpoint{ - Request: stubs.StubHTTPRequest{ - Method: http.MethodGet, - URL: config.getURL, - ExpectedHeaders: stubs.StubAuthHeaders, - ExpectedQuery: config.getConfig.ExpectedQuery, - }, - Response: stubs.StubHTTPResponse{ - Status: config.getConfig.ResponseStatus, - Body: config.getConfig.ResponseBody, - }, - }, stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, diff --git a/internal/cmd/get/command.go b/internal/cmd/get/command.go index 21c5817..2937278 100644 --- a/internal/cmd/get/command.go +++ b/internal/cmd/get/command.go @@ -90,7 +90,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageType)}, ), }, @@ -104,7 +104,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageBuildType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.ImageBuildType)}, ), }, @@ -132,7 +132,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobType)}, ), }, @@ -146,7 +146,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobSubmissionType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.JobSubmissionType)}, ), }, @@ -202,7 +202,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.SnapshotType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.SnapshotType)}, ), }, @@ -244,7 +244,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.WorkbenchType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.WorkbenchType)}, ), }, @@ -280,7 +280,7 @@ func getSubcommands( i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.VolumeType)}, ), Long: i18n.Format( - i18n.CmdGetTypeDescription, + i18n.CmdGetTypeDescriptionWithName, i18n.A{constants.I18nArgTypeKey: types.Titler.String(constants.VolumeType)}, ), }, diff --git a/internal/cmd/log/log_test.go b/internal/cmd/log/log_test.go index fe000f5..452fc1c 100644 --- a/internal/cmd/log/log_test.go +++ b/internal/cmd/log/log_test.go @@ -61,19 +61,20 @@ func TestLogCommand(t *testing.T) { name: "new log function image build list non-200 from server", input: logInput{ resourceType: constants.ImageBuildsType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"image-build-1"}, }, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(image-build-1),name.in.(image-build-1))", ResponseStatus: http.StatusInternalServerError, ResponseBody: `resource-error`, }, }), expected: logExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: "GET ${URL}/postgrest/image_builds?controller_id=in.%28controller-1%29 failed\n" + + err: "GET ${URL}/postgrest/image_builds" + + "?or=%28controller_id.in.%28image-build-1%29%2Cname.in.%28image-build-1%29%29 failed\n" + " Caused by:\n" + "\t* 500 resource-error", resourceCalls: 1}, @@ -82,12 +83,12 @@ func TestLogCommand(t *testing.T) { name: "new log function image build raw log non-200 from server", input: logInput{ resourceType: constants.ImageBuildsType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"image-build-1"}, }, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(image-build-1),name.in.(image-build-1))", ResponseStatus: http.StatusOK, ResponseBody: logImageBuildsResponseBody, }, @@ -108,19 +109,20 @@ func TestLogCommand(t *testing.T) { name: "new log function job submission list non-200 from server", input: logInput{ resourceType: constants.JobSubmissionsType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"job-submission-1"}, }, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(job-submission-1),job_name.in.(job-submission-1))", ResponseStatus: http.StatusInternalServerError, ResponseBody: "resource-error", }, }), expected: logExpected{ displays: []string{stubs.ExpectedDeviceAuthDisplay}, - err: "GET ${URL}/postgrest/job_runs?controller_id=in.%28controller-1%29 failed\n" + + err: "GET ${URL}/postgrest/job_runs" + + "?or=%28controller_id.in.%28job-submission-1%29%2Cjob_name.in.%28job-submission-1%29%29 failed\n" + " Caused by:\n" + "\t* 500 resource-error", resourceCalls: 1}, @@ -129,12 +131,12 @@ func TestLogCommand(t *testing.T) { name: "new log function job submission raw log non-200 from server", input: logInput{ resourceType: constants.JobSubmissionsType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"job-submission-1"}, }, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(job-submission-1),job_name.in.(job-submission-1))", ResponseStatus: http.StatusOK, ResponseBody: logJobSubmissionsResponseBody, }, @@ -153,11 +155,11 @@ func TestLogCommand(t *testing.T) { }, { name: "new log function prints image build logs by singular type", - input: logInput{resourceType: constants.ImageBuildType, resourceIDs: []string{"controller-1"}}, + input: logInput{resourceType: constants.ImageBuildType, resourceIDs: []string{"image-build-1"}}, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(image-build-1),name.in.(image-build-1))", ResponseStatus: http.StatusOK, ResponseBody: logImageBuildsResponseBody, }, @@ -177,12 +179,12 @@ func TestLogCommand(t *testing.T) { name: "new log function prints image build logs by plural type", input: logInput{ resourceType: constants.ImageBuildsType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"image-build-1"}, }, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(image-build-1),name.in.(image-build-1))", ResponseStatus: http.StatusOK, ResponseBody: logImageBuildsResponseBody, }, @@ -200,11 +202,11 @@ func TestLogCommand(t *testing.T) { }, { name: "new log function prints job submission logs by singular type", - input: logInput{resourceType: constants.JobSubmissionType, resourceIDs: []string{"controller-2"}}, + input: logInput{resourceType: constants.JobSubmissionType, resourceIDs: []string{"job-submission-1"}}, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-2)", + ExpectedQuery: "or=(controller_id.in.(job-submission-1),job_name.in.(job-submission-1))", ResponseStatus: http.StatusOK, ResponseBody: logJobSubmissionsResponseBody, }, @@ -222,11 +224,11 @@ func TestLogCommand(t *testing.T) { }, { name: "new log function prints job submission logs by plural type", - input: logInput{resourceType: constants.JobSubmissionsType, resourceIDs: []string{"controller-2"}}, + input: logInput{resourceType: constants.JobSubmissionsType, resourceIDs: []string{"job-submission-1"}}, mockEndpoints: getLogEndpoints(logConfig{ resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), getResourceConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-2)", + ExpectedQuery: "or=(controller_id.in.(job-submission-1),job_name.in.(job-submission-1))", ResponseStatus: http.StatusOK, ResponseBody: logJobSubmissionsResponseBody, }, @@ -253,6 +255,38 @@ func TestLogCommand(t *testing.T) { err: expectedInvalidEndpointErr, }, }, + { + name: "empty job submission log url", + input: logInput{resourceType: constants.JobSubmissionsType, resourceIDs: []string{"job-submission-2"}}, + mockEndpoints: getLogEndpoints(logConfig{ + resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), + getResourceConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: "or=(controller_id.in.(job-submission-2),job_name.in.(job-submission-2))", + ResponseStatus: http.StatusOK, + ResponseBody: logJobSubmissionsResponseBody, + }, + }), + expected: logExpected{ + displays: []string{stubs.ExpectedDeviceAuthDisplay}, + resourceCalls: 1, + }, + }, + { + name: "empty build log url", + input: logInput{resourceType: constants.ImageBuildType, resourceIDs: []string{"image-build-2"}}, + mockEndpoints: getLogEndpoints(logConfig{ + resourceURL: postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), + getResourceConfig: stubs.StubHTTPPartialGetConfig{ + ExpectedQuery: "or=(controller_id.in.(image-build-2),name.in.(image-build-2))", + ResponseStatus: http.StatusOK, + ResponseBody: logImageBuildsResponseBody, + }, + }), + expected: logExpected{ + displays: []string{stubs.ExpectedDeviceAuthDisplay}, + resourceCalls: 1, + }, + }, } for _, tt := range tests { diff --git a/internal/cmd/rebuild/rebuild_test.go b/internal/cmd/rebuild/rebuild_test.go index 169c4fb..33d20de 100644 --- a/internal/cmd/rebuild/rebuild_test.go +++ b/internal/cmd/rebuild/rebuild_test.go @@ -29,7 +29,8 @@ const ( "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" expectedInvalidResourceErr = "" + - "GET ${URL}/postgrest/images?controller_id=in.%28controller-1%29 failed" + + "GET ${URL}/postgrest/images?" + + "or=%28controller_id.in.%28image-1%29%2Cname.in.%28image-1%29%29 failed" + "\n Caused by:" + "\n\t* invalid character 'i' looking for beginning of value" ) @@ -59,10 +60,10 @@ func TestRebuildCommand(t *testing.T) { tests := []rebuildTestCase{ { name: "rebuild images by singular type", - input: rebuildInput{resourceType: constants.ImageType, resourceIDs: []string{"controller-1"}}, + input: rebuildInput{resourceType: constants.ImageType, resourceIDs: []string{"image-1", "image-2"}}, mockEndpoints: getRebuildEndpoints(rebuildConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(image-1,image-2),name.in.(image-1,image-2))", ResponseStatus: http.StatusOK, ResponseBody: rebuildImagesResponseBody, }, @@ -79,10 +80,10 @@ func TestRebuildCommand(t *testing.T) { }, { name: "rebuild images by plural type", - input: rebuildInput{resourceType: constants.ImagesType, resourceIDs: []string{"controller-2"}}, + input: rebuildInput{resourceType: constants.ImagesType, resourceIDs: []string{"image-1", "image-2"}}, mockEndpoints: getRebuildEndpoints(rebuildConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-2)", + ExpectedQuery: "or=(controller_id.in.(image-1,image-2),name.in.(image-1,image-2))", ResponseStatus: http.StatusOK, ResponseBody: rebuildImagesResponseBody, }, @@ -99,10 +100,10 @@ func TestRebuildCommand(t *testing.T) { }, { name: "returns error when rebuild call fails", - input: rebuildInput{resourceType: constants.ImagesType, resourceIDs: []string{"controller-2"}}, + input: rebuildInput{resourceType: constants.ImagesType, resourceIDs: []string{"image-1"}}, mockEndpoints: getRebuildEndpoints(rebuildConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-2)", + ExpectedQuery: "or=(controller_id.in.(image-1),name.in.(image-1))", ResponseStatus: http.StatusOK, ResponseBody: rebuildImagesResponseBody, }, @@ -122,7 +123,7 @@ func TestRebuildCommand(t *testing.T) { input: rebuildInput{ endpoint: "a\nb", resourceType: constants.ImageType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"image-1"}, }, expected: rebuildExpected{ err: expectedInvalidEndpointErr, @@ -132,11 +133,11 @@ func TestRebuildCommand(t *testing.T) { name: "invalid image response", input: rebuildInput{ resourceType: constants.ImageType, - resourceIDs: []string{"controller-1"}, + resourceIDs: []string{"image-1"}, }, mockEndpoints: getRebuildEndpoints(rebuildConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(image-1),name.in.(image-1))", ResponseStatus: http.StatusOK, ResponseBody: `[invalid`, }, diff --git a/internal/cmd/state/state.go b/internal/cmd/state/state.go index 53d65a1..a82f140 100644 --- a/internal/cmd/state/state.go +++ b/internal/cmd/state/state.go @@ -105,12 +105,7 @@ func setWorkbenchDesiredState( desiredState string, svc *workbench.WorkbenchService, ) error { - workbenches, err := workbench.GetWorkbenches(ctx, svc, ids, "") - if err != nil { - return err - } - resolvedIDs := getIDs(workbenches) - changedIDs, err := bulkUpdateState(ctx, svc, desiredState, resolvedIDs) + changedIDs, err := bulkUpdateState(ctx, svc, desiredState, ids) if err != nil { return err } diff --git a/internal/cmd/state/state_test.go b/internal/cmd/state/state_test.go index 149983e..1018c1e 100644 --- a/internal/cmd/state/state_test.go +++ b/internal/cmd/state/state_test.go @@ -51,7 +51,8 @@ const ( "\n Caused by:" + "\n\t* parse \"a\\nb\": net/url: invalid control character in URL" expectedInvalidResourceErr = "" + - "GET ${URL}/postgrest/workbenches?controller_id=in.%28controller-1%29 failed" + + "GET ${URL}/postgrest/workbenches?" + + "or=%28controller_id.in.%28controller-1%29%2Cname.in.%28controller-1%29%29 failed" + "\n Caused by:" + "\n\t* invalid character 'i' looking for beginning of value" expectedInvalidUpdateResponseErr = "" + @@ -112,7 +113,7 @@ func TestStateCommands(t *testing.T) { }, mockEndpoints: getStateEndpoints(stateConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1,controller-2)", + ExpectedQuery: "or=(controller_id.in.(controller-1,controller-2),name.in.(controller-1,controller-2))", ResponseStatus: http.StatusOK, ResponseBody: stateGetResponseBodySingular, }, @@ -136,7 +137,7 @@ func TestStateCommands(t *testing.T) { }, mockEndpoints: getStateEndpoints(stateConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-3)", + ExpectedQuery: "or=(controller_id.in.(controller-3),name.in.(controller-3))", ResponseStatus: http.StatusOK, ResponseBody: stateGetResponseBodyPlural, }, @@ -171,7 +172,7 @@ func TestStateCommands(t *testing.T) { }, mockEndpoints: getStateEndpoints(stateConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-1)", + ExpectedQuery: "or=(controller_id.in.(controller-1),name.in.(controller-1))", ResponseStatus: http.StatusOK, ResponseBody: `[invalid`, }, @@ -190,7 +191,7 @@ func TestStateCommands(t *testing.T) { }, mockEndpoints: getStateEndpoints(stateConfig{ getConfig: stubs.StubHTTPPartialGetConfig{ - ExpectedQuery: "controller_id=in.(controller-3)", + ExpectedQuery: "or=(controller_id.in.(controller-3),name.in.(controller-3))", ResponseStatus: http.StatusOK, ResponseBody: stateGetResponseBodyPlural, }, diff --git a/internal/i18n/i18n_messages_workbench_cli.go b/internal/i18n/i18n_messages_workbench_cli.go index 6f80a4b..f67256d 100644 --- a/internal/i18n/i18n_messages_workbench_cli.go +++ b/internal/i18n/i18n_messages_workbench_cli.go @@ -15,7 +15,7 @@ var wbctlMessages = "\n" + "wbctl-cli-icu.client.id.must.be.specified.msg=--client-id must be specified\n" + "wbctl-cli-icu.cmd.api.usage.msg=Directly call Workbench API endpoints\n" + "wbctl-cli-icu.cmd.cancel.desc=Cancels one or many Workbench resources\n" + - "wbctl-cli-icu.cmd.cancel.type.description.msg=Cancels the {type} resources identified by a list of resource ids. At least one id is required.\n" + + "wbctl-cli-icu.cmd.cancel.type.description.msg=Cancels the {type} resources identified by a list of resource ids or names. At least one id or name is required.\n" + "wbctl-cli-icu.cmd.cancel.type.usage.msg=Cancel one or many {type} resources\n" + "wbctl-cli-icu.cmd.cancel.usage.msg=Cancel running Workbench resources\n" + "wbctl-cli-icu.cmd.create.desc=Creates Workbench resources.\n" + @@ -23,23 +23,25 @@ var wbctlMessages = "\n" + "wbctl-cli-icu.cmd.create.type.usage.msg=Create a new {type} resource\n" + "wbctl-cli-icu.cmd.create.usage.msg=Create Workbench resources\n" + "wbctl-cli-icu.cmd.delete.desc=Deletes one or many Workbench resources.\n" + - "wbctl-cli-icu.cmd.delete.type.description.msg=Deletes the {type} resources identified by a list of resource ids. At least one id is required.\n" + + "wbctl-cli-icu.cmd.delete.type.description.msg=Deletes the {type} resources identified by a list of resource ids or names. At least one id or name is required.\n" + "wbctl-cli-icu.cmd.delete.type.usage.msg=Delete one or many {type} resources\n" + "wbctl-cli-icu.cmd.delete.usage.msg=Delete Workbench resources\n" + "wbctl-cli-icu.cmd.describe.desc=Print a detailed description of one or many Workbench resources.\n" + "wbctl-cli-icu.cmd.describe.type.description.msg=Print detailed information about the {type} resources identified by an optional list of resource ids. All resources printed if no ids provided.\n" + + "wbctl-cli-icu.cmd.describe.type.description.with.name.msg=Print detailed information about the {type} resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided.\n" + "wbctl-cli-icu.cmd.describe.type.usage.msg=Describe one or many {type} resources\n" + "wbctl-cli-icu.cmd.describe.usage.msg=Describe Workbench resources in detail\n" + "wbctl-cli-icu.cmd.edit.description.msg=Edit a Workbench resource using the default editor.\n" + - "wbctl-cli-icu.cmd.edit.type.description.msg=Edit one {type} resource identified by id. One and only one id is required.\n" + + "wbctl-cli-icu.cmd.edit.type.description.msg=Edit one {type} resource identified by id or name. One and only one id or name is required.\n" + "wbctl-cli-icu.cmd.edit.type.usage.msg=Edit a {type} resource\n" + "wbctl-cli-icu.cmd.edit.usage.msg=Edit Workbench resources\n" + "wbctl-cli-icu.cmd.get.desc=Print a table of basic information about one or many Workbench resources.\n" + "wbctl-cli-icu.cmd.get.type.description.msg=Print a table of basic information about the {type} resources identified by an optional list of resource ids. All resources printed if no ids provided.\n" + + "wbctl-cli-icu.cmd.get.type.description.with.name.msg=Print a table of basic information about the {type} resources identified by an optional list of resource ids or names. All resources printed if no ids or names provided.\n" + "wbctl-cli-icu.cmd.get.type.usage.msg=Get one or many {type} resources\n" + "wbctl-cli-icu.cmd.get.usage.msg=Print Workbench resources\n" + "wbctl-cli-icu.cmd.log.desc=Fetches the logs of one or many Workbench resources\n" + - "wbctl-cli-icu.cmd.log.type.description.msg=Print the log of the {type} resources identified a list of resource ids. At least one id is required.\n" + + "wbctl-cli-icu.cmd.log.type.description.msg=Print the log of the {type} resources identified by a list of resource ids or names. At least one id or name is required.\n" + "wbctl-cli-icu.cmd.log.type.usage.msg=Print the log of one or many {type} resources\n" + "wbctl-cli-icu.cmd.log.usage.msg=Fetch the logs of Workbench resources\n" + "wbctl-cli-icu.cmd.login.desc=Authenticates with the Workbench API and stores the resulting token in ~/.wbctl/token.json so that subsequent commands do not require repeated authorization.\n" + @@ -55,21 +57,21 @@ var wbctlMessages = "\n" + "wbctl-cli-icu.cmd.profile.show.usage.msg=Show current profile\n" + "wbctl-cli-icu.cmd.profile.usage.msg=Manage profiles\n" + "wbctl-cli-icu.cmd.rebuild.desc=Rebuilds one or many Workbench resources\n" + - "wbctl-cli-icu.cmd.rebuild.type.description.msg=Rebuild the {type} resources identified by a list of resource ids. At least one id is required.\n" + + "wbctl-cli-icu.cmd.rebuild.type.description.msg=Rebuild the {type} resources identified by a list of resource ids or names. At least one id or name is required.\n" + "wbctl-cli-icu.cmd.rebuild.type.usage.msg=Rebuild one or many {type} resources\n" + "wbctl-cli-icu.cmd.rebuild.usage.msg=Rebuild Workbench image\n" + "wbctl-cli-icu.cmd.settings.description.msg=Displays or sets user settings.\n" + "wbctl-cli-icu.cmd.settings.usage.msg=Manage user settings\n" + "wbctl-cli-icu.cmd.start.desc=Starts one or many Workbench resources\n" + - "wbctl-cli-icu.cmd.start.type.description.msg=Start the {type} resources identified by a list of resource ids. At least one id is required.\n" + + "wbctl-cli-icu.cmd.start.type.description.msg=Start the {type} resources identified by a list of resource ids or names. At least one id or name is required.\n" + "wbctl-cli-icu.cmd.start.type.usage.msg=Start one or many {type} resources\n" + "wbctl-cli-icu.cmd.start.usage.msg=Start stopped Workbench resources\n" + "wbctl-cli-icu.cmd.stop.desc=Stops one or many Workbench resources\n" + - "wbctl-cli-icu.cmd.stop.type.description.msg=Stop the {type} resources identified by a list of resource ids. At least one id is required.\n" + + "wbctl-cli-icu.cmd.stop.type.description.msg=Stop the {type} resources identified by a list of resource ids or names. At least one id or name is required.\n" + "wbctl-cli-icu.cmd.stop.type.usage.msg=Stop one or many {type} resources\n" + "wbctl-cli-icu.cmd.stop.usage.msg=Stop running Workbench resources\n" + "wbctl-cli-icu.cmd.update.desc=Updates one or many Workbench resources.\n" + - "wbctl-cli-icu.cmd.update.type.description.msg=Updates all the {type} resources identified by a list of ids with the options provided.\n" + + "wbctl-cli-icu.cmd.update.type.description.msg=Updates all the {type} resources identified by a list of ids or names with the options provided.\n" + "wbctl-cli-icu.cmd.update.type.usage.msg=Update one or many {type} resources\n" + "wbctl-cli-icu.cmd.update.usage.msg=Update Workbench resources\n" + "wbctl-cli-icu.cmd.version.description.msg=Print version information of the wbctl binary.\n" + diff --git a/internal/i18n/logmsg.go b/internal/i18n/logmsg.go index cd62a74..dc53005 100644 --- a/internal/i18n/logmsg.go +++ b/internal/i18n/logmsg.go @@ -40,6 +40,7 @@ const ( CmdDeleteUsage = BundleID + ".cmd.delete.usage.msg" CmdDescribeDescription = BundleID + ".cmd.describe.desc" CmdDescribeTypeDescription = BundleID + ".cmd.describe.type.description.msg" + CmdDescribeTypeDescriptionWithName = BundleID + ".cmd.describe.type.description.with.name.msg" CmdDescribeTypeUsage = BundleID + ".cmd.describe.type.usage.msg" CmdDescribeUsage = BundleID + ".cmd.describe.usage.msg" CmdEditDescription = BundleID + ".cmd.edit.description.msg" @@ -48,6 +49,7 @@ const ( CmdEditUsage = BundleID + ".cmd.edit.usage.msg" CmdGetDescription = BundleID + ".cmd.get.desc" CmdGetTypeDescription = BundleID + ".cmd.get.type.description.msg" + CmdGetTypeDescriptionWithName = BundleID + ".cmd.get.type.description.with.name.msg" CmdGetTypeUsage = BundleID + ".cmd.get.type.usage.msg" CmdGetUsage = BundleID + ".cmd.get.usage.msg" CmdLogDescription = BundleID + ".cmd.log.desc" diff --git a/internal/services/workbench/image_builds.go b/internal/services/workbench/image_builds.go index bc3447d..4585b23 100644 --- a/internal/services/workbench/image_builds.go +++ b/internal/services/workbench/image_builds.go @@ -19,21 +19,28 @@ import ( ) // GetImageBuilds gets the image builds in the given WorkbenchService. If given -// any ids, only the matching resources will be returned. Otherwise, all +// any ids/names, only the matching resources will be returned. Otherwise, all // resources are returned. func GetImageBuilds( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.ImageBuilds, error) { - logutil.GetLogger().Info("getting image-builds", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting image-builds", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, }) - return result.Split(Get[types.ImageBuilds]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), IDsOrControllerIDsQuery(ids, sortSpec), + imageBuilds, err := result.Split(Get[types.ImageBuilds]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointImageBuilds), IDsControllerIDsOrNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + imageBuilds, + ), nil } // PrintImageBuilds returns a function that prints the given image builds. diff --git a/internal/services/workbench/image_builds_test.go b/internal/services/workbench/image_builds_test.go index ac3ed11..1dd3888 100644 --- a/internal/services/workbench/image_builds_test.go +++ b/internal/services/workbench/image_builds_test.go @@ -6,7 +6,6 @@ package workbench_test import ( "context" "encoding/json" - "fmt" "net/http" "strings" "testing" @@ -51,6 +50,13 @@ const ( "controller_id": "controller-build-2", "created_by": "carol", "image_id": "img-controller" + }, + { + "id": "ib-name", + "name": "build-name", + "controller_id": "ctrl-name", + "created_by": "ted", + "image_id": "img-name" } ] ` @@ -211,6 +217,13 @@ var ( CreatedBy: "carol", ImageID: "img-controller", }, + { + ID: "ib-name", + Name: "build-name", + ControllerID: "ctrl-name", + CreatedBy: "ted", + ImageID: "img-name", + }, } ) @@ -225,13 +238,15 @@ func TestGetImageBuilds(t *testing.T) { }, }, { - name: "get image builds success with mixed uuid and controller ids query", + name: "get image builds success with mixed uuid, controller id, names query", request: getRequest{ - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-build-2"}, + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-build-2", "build-name"}, sort: "id.asc", }, mockEndpoints: getImageBuildsGetEndpoints( - "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),controller_id.in.(controller-build-2))&order=id.asc", + "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ + "controller_id.in.(controller-build-2,build-name),"+ + "name.in.(123e4567-e89b-12d3-a456-426614174000,controller-build-2,build-name))&order=id.asc", http.StatusOK, imageBuildsGetMixedIDsResponseJSON, ), @@ -298,9 +313,9 @@ func TestGetImageBuilds(t *testing.T) { request: getRequest{ ids: []string{"id-a"}, }, - mockEndpoints: getImageBuildsGetEndpoints("controller_id=in.(id-a)", http.StatusOK, `[`), + mockEndpoints: getImageBuildsGetEndpoints("or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusOK, `[`), expected: getExpected[types.ImageBuilds]{ - err: "GET %s/postgrest/image_builds?controller_id=in.%%28id-a%%29 failed\n" + + err: "GET ${URL}/postgrest/image_builds?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF", }, @@ -310,9 +325,13 @@ func TestGetImageBuilds(t *testing.T) { request: getRequest{ ids: []string{"id-a"}, }, - mockEndpoints: getImageBuildsGetEndpoints("controller_id=in.(id-a)", http.StatusUnauthorized, `unauthorized`), + mockEndpoints: getImageBuildsGetEndpoints( + "or=(controller_id.in.(id-a),name.in.(id-a))", + http.StatusUnauthorized, + `unauthorized`, + ), expected: getExpected[types.ImageBuilds]{ - err: "GET %s/postgrest/image_builds?controller_id=in.%%28id-a%%29 failed\n" + + err: "GET ${URL}/postgrest/image_builds?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -327,7 +346,7 @@ func TestGetImageBuilds(t *testing.T) { actual, err := workbench.GetImageBuilds(context.Background(), svc, tt.request.ids, tt.request.sort) if tt.expected.err != "" { - require.EqualError(t, err, fmt.Sprintf(tt.expected.err, srv.URL)) + require.EqualError(t, err, strings.ReplaceAll(tt.expected.err, "${URL}", srv.URL)) return } diff --git a/internal/services/workbench/images.go b/internal/services/workbench/images.go index bee5de1..bf10ffc 100644 --- a/internal/services/workbench/images.go +++ b/internal/services/workbench/images.go @@ -19,16 +19,16 @@ import ( "github.com/sassoftware/wbctl/internal/ui" ) -// GetImages gets the images in the given WorkbenchService. If given any ids, -// only the matching resources will be returned. Otherwise, all resources are -// returned. +// GetImages gets the images in the given WorkbenchService. If given any +// ids/names, only the matching resources will be returned. Otherwise, all +// resources are returned. func GetImages( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.Images, error) { - logutil.GetLogger().Info("getting images", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting images", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, constants.SortKeyImageBuildID: {constants.SortKeyLatestImageBuildID}, @@ -39,14 +39,31 @@ func GetImages( constants.SortKeyImageTag, }, }) - return result.Split(Get[types.Images]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointImages), IDsOrControllerIDsQuery(ids, sortSpec), + images, err := result.Split(Get[types.Images]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointImages), IDsControllerIDsOrNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + images, + ), nil +} + +func resolveImageIDs(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + return resolveIDs( + ctx, s, args, GetImages, types.Image.GetID, constants.ImageType, + ) } -// BulkDeleteImages deletes the images with the given ids. -func BulkDeleteImages(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { - logutil.GetLogger().Info("deleting images", "ids", ids) +// BulkDeleteImages deletes the images with the given ids/names. +func BulkDeleteImages(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + logutil.GetLogger().Info("deleting images", "args", args) + ids, err := resolveImageIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[[]string]( ctx, s, @@ -57,7 +74,7 @@ func BulkDeleteImages(ctx context.Context, s *WorkbenchService, ids []string) ([ // CreateImage creates an image. func CreateImage(ctx context.Context, s *WorkbenchService, body types.Image) (types.Image, error) { - logutil.GetLogger().Info("deleting images", "body", body) + logutil.GetLogger().Info("creating image", "body", body) return result.Split(Post[types.Image]( ctx, s, @@ -75,14 +92,18 @@ func RebuildImage(ctx context.Context, s *WorkbenchService, id string) (bool, er } // BulkUpdateImages applies the given types.Image to all the images with the -// given ids. +// given ids/names. func BulkUpdateImages( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, body types.Image, ) (types.Images, error) { - logutil.GetLogger().Info("updating images", "ids", ids, "body", body) + logutil.GetLogger().Info("updating images", "args", args, "body", body) + ids, err := resolveImageIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[types.Images]( ctx, s, diff --git a/internal/services/workbench/images_test.go b/internal/services/workbench/images_test.go index d82184e..d287b08 100644 --- a/internal/services/workbench/images_test.go +++ b/internal/services/workbench/images_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/sassoftware/wbctl/internal/constants" + "github.com/sassoftware/wbctl/internal/i18n" "github.com/sassoftware/wbctl/internal/postgrest" "github.com/sassoftware/wbctl/internal/services/workbench" "github.com/sassoftware/wbctl/internal/stubs" @@ -36,7 +37,7 @@ const ( ] ` - imagesGetTwoResponseJSON = ` + imagesGetMixedResponseJSON = ` [ { "id": "123e4567-e89b-12d3-a456-426614174000", @@ -51,6 +52,13 @@ const ( "image_name": "app-controller", "image_registry": "registry.controller.com", "controller_id": "controller-app-2" + }, + { + "id": "img-name", + "name": "image-name", + "image_name": "image-name", + "image_registry": "registry.name.com", + "controller_id": "ctrl-name" } ] ` @@ -241,8 +249,6 @@ const ( ] ` - imagesDeleteSuccessResponseJSON = `["img-1", "img-2"]` - expectedImagesEmptyTableNonAdmin = "" + "ID\tCONTROLLER_ID\tNAME\tDESCRIPTION\tIMAGE\tIMAGE_BUILD_ID\tSTATE\tMESSAGE\tAGE" @@ -347,6 +353,9 @@ Pod pod-1: } ] ` + imagesResolveIDsQuery = "or=(controller_id.in.(img-1,img-2),name.in.(img-1,img-2))" + imagesResolveIDsResponseJSON = `[{"id":"img-1"},{"id":"img-2"}]` + imagesDeleteSuccessResponseJSON = `["img-1", "img-2"]` expectedDeleteImageRequestJSON = `{"ids":["img-1","img-2"]}` expectedCreateImageRequestJSON = `{"param":{"name":"image-1"}}` createImageOneResponseBodyJSON = `{"id":"img-1","name":"image-1"}` @@ -365,7 +374,7 @@ var ( }, } - expectedResponseTwoImages = types.Images{ + expectedResponseMixedImages = types.Images{ { ID: "123e4567-e89b-12d3-a456-426614174000", Name: "app-uuid", @@ -380,6 +389,13 @@ var ( ImageRegistry: "registry.controller.com", ControllerID: "controller-app-2", }, + { + ID: "img-name", + Name: "image-name", + ImageName: "image-name", + ImageRegistry: "registry.name.com", + ControllerID: "ctrl-name", + }, } ) @@ -391,6 +407,8 @@ func TestDeleteImage(t *testing.T) { ids: []string{"img-1", "img-2"}, }, mockEndpoints: getImagesDeleteEndpoints( + imagesResolveIDsQuery, + imagesResolveIDsResponseJSON, expectedDeleteImageRequestJSON, http.StatusOK, imagesDeleteSuccessResponseJSON, @@ -405,6 +423,8 @@ func TestDeleteImage(t *testing.T) { ids: []string{"img-1", "img-2"}, }, mockEndpoints: getImagesDeleteEndpoints( + imagesResolveIDsQuery, + imagesResolveIDsResponseJSON, expectedDeleteImageRequestJSON, http.StatusUnauthorized, `unauthorized`, @@ -415,6 +435,25 @@ func TestDeleteImage(t *testing.T) { "\t* 401 unauthorized", }, }, + { + name: "delete image not found", + request: deleteRequest{ + ids: []string{"missing"}, + }, + mockEndpoints: getImagesDeleteEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: deleteExpected{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.ImageType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { @@ -540,6 +579,23 @@ func TestRebuildImage(t *testing.T) { func TestBulkUpdateImages(t *testing.T) { tests := []bulkupdateTestCase[types.Image, types.Images]{ + { + name: "bulk update images not found", + request: bulkupdateRequest[types.Image]{ids: []string{"missing"}, body: types.Image{Name: "updated-name"}}, + mockEndpoints: getImagesBulkUpdateEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: bulkupdateExpected[types.Images]{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.ImageType, + constants.I18nArgKeyID: "missing", + }), + }, + }, { name: "bulk update images success", request: bulkupdateRequest[types.Image]{ @@ -547,6 +603,8 @@ func TestBulkUpdateImages(t *testing.T) { body: types.Image{Name: "updated-name"}, }, mockEndpoints: getImagesBulkUpdateEndpoints( + imagesResolveIDsQuery, + imagesResolveIDsResponseJSON, expectedBulkUpdateImageRequestJSON, http.StatusOK, imagesBulkUpdateResponseJSON, @@ -561,6 +619,8 @@ func TestBulkUpdateImages(t *testing.T) { name: "bulk update images non-200 from server", request: bulkupdateRequest[types.Image]{ids: []string{"img-1", "img-2"}, body: types.Image{Name: "updated-name"}}, mockEndpoints: getImagesBulkUpdateEndpoints( + imagesResolveIDsQuery, + imagesResolveIDsResponseJSON, expectedBulkUpdateImageRequestJSON, http.StatusUnauthorized, `unauthorized`, @@ -575,6 +635,8 @@ func TestBulkUpdateImages(t *testing.T) { name: "bulk update images invalid json response", request: bulkupdateRequest[types.Image]{ids: []string{"img-1", "img-2"}, body: types.Image{Name: "updated-name"}}, mockEndpoints: getImagesBulkUpdateEndpoints( + imagesResolveIDsQuery, + imagesResolveIDsResponseJSON, expectedBulkUpdateImageRequestJSON, http.StatusOK, `[`, @@ -664,18 +726,20 @@ func TestGetImages(t *testing.T) { expected: getExpected[types.Images]{result: expectedResponseOneImage}, }, { - name: "get images success with mixed uuid and controller ids query", + name: "get images success with mixed uuid, controller id, names query", request: getRequest{ - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-app-2"}, + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-app-2", "image-name"}, sort: "id.asc", }, mockEndpoints: getImagesGetEndpoints( - "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),controller_id.in.(controller-app-2))&order=id.asc", + "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ + "controller_id.in.(controller-app-2,image-name),"+ + "name.in.(123e4567-e89b-12d3-a456-426614174000,controller-app-2,image-name))&order=id.asc", http.StatusOK, - imagesGetTwoResponseJSON, + imagesGetMixedResponseJSON, ), expected: getExpected[types.Images]{ - result: expectedResponseTwoImages, + result: expectedResponseMixedImages, }, }, { @@ -755,18 +819,20 @@ func TestGetImages(t *testing.T) { { name: "get images invalid json response", request: getRequest{ids: []string{"id-a"}}, - mockEndpoints: getImagesGetEndpoints("controller_id=in.(id-a)", http.StatusOK, "["), + mockEndpoints: getImagesGetEndpoints("or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusOK, "["), expected: getExpected[types.Images]{ - err: "GET ${URL}/postgrest/images?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/images?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF"}, }, { - name: "get images non-200 from server", - request: getRequest{ids: []string{"id-a"}}, - mockEndpoints: getImagesGetEndpoints("controller_id=in.(id-a)", http.StatusUnauthorized, "unauthorized"), + name: "get images non-200 from server", + request: getRequest{ids: []string{"id-a"}}, + mockEndpoints: getImagesGetEndpoints( + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusUnauthorized, "unauthorized", + ), expected: getExpected[types.Images]{ - err: "GET ${URL}/postgrest/images?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/images?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized"}, }, @@ -886,12 +952,14 @@ func createTokenWithRole(role string) string { } func getImagesDeleteEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getImagesGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCImageBulkDelete), @@ -902,8 +970,7 @@ func getImagesDeleteEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getImagesCreateEndpoints( @@ -928,12 +995,14 @@ func getImagesCreateEndpoints( } func getImagesBulkUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getImagesGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCImageBulkUpdate), @@ -944,8 +1013,7 @@ func getImagesBulkUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getImagesGetEndpoints( diff --git a/internal/services/workbench/job_submissions.go b/internal/services/workbench/job_submissions.go index 5d026a1..bcb661d 100644 --- a/internal/services/workbench/job_submissions.go +++ b/internal/services/workbench/job_submissions.go @@ -21,27 +21,45 @@ import ( ) // GetJobSubmissions gets the job submissions in the given WorkbenchService. If -// given any ids, only the matching resources will be returned. Otherwise, all +// given any ids/names, only the matching resources will be returned. Otherwise, all // resources are returned. +// nolint:dupl func GetJobSubmissions( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.JobSubmissions, error) { - logutil.GetLogger().Info("getting job-submissions", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting job-submissions", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, constants.SortKeyState: {constants.SortKeyCurrentState}, }) - return result.Split(Get[types.JobSubmissions]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), IDsOrControllerIDsQuery(ids, sortSpec), + jobSubmissions, err := result.Split(Get[types.JobSubmissions]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointJobRuns), IDsControllerIDsOrJobNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + jobSubmissions, + ), nil +} + +func resolveJobSubmissionIDs(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { + return resolveIDs( + ctx, s, ids, GetJobSubmissions, types.JobSubmission.GetID, constants.JobSubmissionType, + ) } -// BulkDeleteJobSubmissions deletes the job submission with the given id. -func BulkDeleteJobSubmissions(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { - logutil.GetLogger().Info("deleting job-submissions", "ids", ids) +// BulkDeleteJobSubmissions deletes the job submissions with the given ids/names. +func BulkDeleteJobSubmissions(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + logutil.GetLogger().Info("deleting job-submissions", "args", args) + ids, err := resolveJobSubmissionIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[[]string]( ctx, s, diff --git a/internal/services/workbench/job_submissions_test.go b/internal/services/workbench/job_submissions_test.go index 22babeb..2b0461d 100644 --- a/internal/services/workbench/job_submissions_test.go +++ b/internal/services/workbench/job_submissions_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/sassoftware/wbctl/internal/constants" + "github.com/sassoftware/wbctl/internal/i18n" "github.com/sassoftware/wbctl/internal/postgrest" "github.com/sassoftware/wbctl/internal/services/workbench" "github.com/sassoftware/wbctl/internal/stubs" @@ -57,6 +58,15 @@ const ( "current_state": "Queued", "runtime": 0.00, "log_url": "https://logs/controller" + }, + { + "id": "js-job-name", + "controller_id": "ctrl-job-name", + "job_name": "job-name-job", + "username": "carol", + "current_state": "Queued", + "runtime": 0.00, + "log_url": "https://logs/controller" } ] ` @@ -334,6 +344,9 @@ Pod pod-1: Nominated node: node-a IPs: 10.0.0.1 ` + jobRunResolveIDsQuery = "or=(controller_id.in.(js-1,js-2),job_name.in.(js-1,js-2))" + jobRunResolveIDsResponseJSON = `[{"id":"js-1"},{"id":"js-2"}]` + jobRunDeleteSuccessResponseJSON = `["js-1", "js-2"]` expectedDeleteJobRunRequestJSON = `{"ids":["js-1", "js-2"]}` expectedCreateJobRunRequestJSON = `{"param":{"job_name":"daily-job"}}` createJobRunOneResponseBodyJSON = `{"id":"js-1","job_name":"daily-job"}` @@ -353,7 +366,7 @@ var ( }, } - expectedResponseTwoSubmissions = types.JobSubmissions{ + expectedResponseMixedSubmissions = types.JobSubmissions{ { ID: "123e4567-e89b-12d3-a456-426614174000", ControllerID: "ctrl-uuid", @@ -372,6 +385,15 @@ var ( Runtime: 0, LogURL: "https://logs/controller", }, + { + ID: "js-job-name", + ControllerID: "ctrl-job-name", + JobName: "job-name-job", + Username: "carol", + CurrentState: "Queued", + Runtime: 0, + LogURL: "https://logs/controller", + }, } ) @@ -383,9 +405,11 @@ func TestDeleteJobSubmission(t *testing.T) { ids: []string{"js-1", "js-2"}, }, mockEndpoints: getJobSubmissionDeleteEndpoints( + jobRunResolveIDsQuery, + jobRunResolveIDsResponseJSON, expectedDeleteJobRunRequestJSON, http.StatusOK, - `["js-1", "js-2"]`, + jobRunDeleteSuccessResponseJSON, ), expected: deleteExpected{ result: []string{"js-1", "js-2"}, @@ -397,6 +421,8 @@ func TestDeleteJobSubmission(t *testing.T) { ids: []string{"js-1", "js-2"}, }, mockEndpoints: getJobSubmissionDeleteEndpoints( + jobRunResolveIDsQuery, + jobRunResolveIDsResponseJSON, expectedDeleteJobRunRequestJSON, http.StatusUnauthorized, "unauthorized", @@ -407,6 +433,25 @@ func TestDeleteJobSubmission(t *testing.T) { "\t* 401 unauthorized", }, }, + { + name: "delete job-submission not found", + request: deleteRequest{ + ids: []string{"missing"}, + }, + mockEndpoints: getJobSubmissionDeleteEndpoints( + `or=(controller_id.in.(missing),job_name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: deleteExpected{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.JobSubmissionType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { @@ -500,6 +545,22 @@ func TestGetJobSubmissions(t *testing.T) { result: expectedResponseOneSubmission, }, }, + { + name: "get success with mixed uuid, controller ids, names query", + request: getRequest{ + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-job-2", "job-name-job"}, + }, + mockEndpoints: getJobSubmissionsGetEndpoints( + "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ + "controller_id.in.(controller-job-2,job-name-job),"+ + "job_name.in.(123e4567-e89b-12d3-a456-426614174000,controller-job-2,job-name-job))", + http.StatusOK, + jobSubmissionsGetMixedIDsResponseJSON, + ), + expected: getExpected[types.JobSubmissions]{ + result: expectedResponseMixedSubmissions, + }, + }, { name: "sort by job_name asc", request: getRequest{ @@ -668,32 +729,18 @@ func TestGetJobSubmissions(t *testing.T) { result: expectedResponseOneSubmission, }, }, - { - name: "success with mixed uuid and controller ids query", - request: getRequest{ - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-job-2"}, - }, - mockEndpoints: getJobSubmissionsGetEndpoints( - "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),controller_id.in.(controller-job-2))", - http.StatusOK, - jobSubmissionsGetMixedIDsResponseJSON, - ), - expected: getExpected[types.JobSubmissions]{ - result: expectedResponseTwoSubmissions, - }, - }, { name: "invalid json response", request: getRequest{ ids: []string{"id-a"}, }, mockEndpoints: getJobSubmissionsGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),job_name.in.(id-a))", http.StatusOK, "[", ), expected: getExpected[types.JobSubmissions]{ - err: "GET ${URL}/postgrest/job_runs?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/job_runs?or=%28controller_id.in.%28id-a%29%2Cjob_name.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF", }, @@ -704,12 +751,12 @@ func TestGetJobSubmissions(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getJobSubmissionsGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),job_name.in.(id-a))", http.StatusUnauthorized, "unauthorized", ), expected: getExpected[types.JobSubmissions]{ - err: "GET ${URL}/postgrest/job_runs?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/job_runs?or=%28controller_id.in.%28id-a%29%2Cjob_name.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -852,12 +899,14 @@ func TestDescribeJobSubmissions(t *testing.T) { } func getJobSubmissionDeleteEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getJobSubmissionsGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCJobRunBulkDelete), @@ -868,8 +917,7 @@ func getJobSubmissionDeleteEndpoints( Body: responseBody, Status: responseStatus, }, - }, - } + }) } func getJobSubmissionCreateEndpoints( diff --git a/internal/services/workbench/jobs.go b/internal/services/workbench/jobs.go index a5b3010..9230363 100644 --- a/internal/services/workbench/jobs.go +++ b/internal/services/workbench/jobs.go @@ -18,28 +18,45 @@ import ( "github.com/sassoftware/wbctl/internal/ui" ) -// GetJobs gets the jobs in the given WorkbenchService. If given any ids, only -// the matching resources will be returned. Otherwise, all resources are +// GetJobs gets the jobs in the given WorkbenchService. If given any ids/names, +// only the matching resources will be returned. Otherwise, all resources are // returned. func GetJobs( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.Jobs, error) { - logutil.GetLogger().Info("getting jobs", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting jobs", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, constants.SortKeyState: {constants.SortKeyCurrentState}, }) - return result.Split(Get[types.Jobs]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointJobs), IDsQuery(ids, sortSpec), + jobs, err := result.Split(Get[types.Jobs]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointJobs), IDsOrNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + jobs, + ), nil +} + +func resolveJobIDs(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + return resolveIDs( + ctx, s, args, GetJobs, types.Job.GetID, constants.JobType, + ) } -// BulkDeleteJobs deletes the job with the given id. -func BulkDeleteJobs(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { - logutil.GetLogger().Info("deleting jobs", "ids", ids) +// BulkDeleteJobs deletes the jobs with the given ids/names. +func BulkDeleteJobs(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + logutil.GetLogger().Info("deleting jobs", "args", args) + ids, err := resolveJobIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[[]string]( ctx, s, @@ -59,15 +76,19 @@ func CreateJob(ctx context.Context, s *WorkbenchService, body types.Job) (types. )) } -// BulkUpdateJobs applies the given types.Job to all the jobs with the given -// ids. +// BulkUpdateJobs applies the given types.Job to all the jobs with the given +// ids/names. func BulkUpdateJobs( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, body types.Job, ) (types.Jobs, error) { - logutil.GetLogger().Info("updating jobs", "ids", ids, "body", body) + logutil.GetLogger().Info("updating jobs", "args", args, "body", body) + ids, err := resolveJobIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[types.Jobs]( ctx, s, diff --git a/internal/services/workbench/jobs_test.go b/internal/services/workbench/jobs_test.go index 00f2c90..dcf4a26 100644 --- a/internal/services/workbench/jobs_test.go +++ b/internal/services/workbench/jobs_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/sassoftware/wbctl/internal/constants" + "github.com/sassoftware/wbctl/internal/i18n" "github.com/sassoftware/wbctl/internal/postgrest" "github.com/sassoftware/wbctl/internal/services/workbench" "github.com/sassoftware/wbctl/internal/stubs" @@ -34,17 +35,17 @@ const ( ] ` - jobsGetTwoResponseJSON = ` + jobsGetMixedResponseJSON = ` [ { - "id": "id-a", - "name": "job-a", - "application_id": "app-a" + "id": "123e4567-e89b-12d3-a456-426614174000", + "name": "job-uuid", + "application_id": "app-uuid" }, { - "id": "id-b", - "name": "job-b", - "application_id": "app-b" + "id": "job-name-id", + "name": "job-name", + "application_id": "app-name" } ] ` @@ -352,6 +353,9 @@ Schedule: 0 * * * * } ] ` + jobsResolveIDsQuery = "or=(name.in.(job-1,job-2))" + jobsResolveIDsResponseJSON = `[{"id":"job-1"},{"id":"job-2"}]` + jobsDeleteSuccessResponseJSON = `["job-1", "job-2"]` expectedDeleteJobRequestJSON = `{"ids":["job-1","job-2"]}` createJobOneResponseBodyJSON = `{"id":"job-1","name":"daily-job"}` expectedCreateJobRequestJSON = `{"param":{"name":"daily-job"}}` @@ -363,9 +367,9 @@ var ( {ID: "job-1", Name: "daily-job", Description: "first", ApplicationID: "app-1"}, } - expectedResponseTwoJobs = types.Jobs{ - {ID: "id-a", Name: "job-a", ApplicationID: "app-a"}, - {ID: "id-b", Name: "job-b", ApplicationID: "app-b"}, + expectedResponseMixedJobs = types.Jobs{ + {ID: "123e4567-e89b-12d3-a456-426614174000", Name: "job-uuid", ApplicationID: "app-uuid"}, + {ID: "job-name-id", Name: "job-name", ApplicationID: "app-name"}, } ) @@ -377,9 +381,11 @@ func TestDeleteJob(t *testing.T) { ids: []string{"job-1", "job-2"}, }, mockEndpoints: getJobsDeleteEndpoints( + jobsResolveIDsQuery, + jobsResolveIDsResponseJSON, expectedDeleteJobRequestJSON, http.StatusOK, - `["job-1", "job-2"]`, + jobsDeleteSuccessResponseJSON, ), expected: deleteExpected{ result: []string{"job-1", "job-2"}, @@ -391,6 +397,8 @@ func TestDeleteJob(t *testing.T) { ids: []string{"job-1", "job-2"}, }, mockEndpoints: getJobsDeleteEndpoints( + jobsResolveIDsQuery, + jobsResolveIDsResponseJSON, expectedDeleteJobRequestJSON, http.StatusUnauthorized, "unauthorized", @@ -401,6 +409,25 @@ func TestDeleteJob(t *testing.T) { "\t* 401 unauthorized", }, }, + { + name: "delete jobs not found", + request: deleteRequest{ + ids: []string{"missing"}, + }, + mockEndpoints: getJobsDeleteEndpoints( + `or=(name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: deleteExpected{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.JobType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { @@ -489,6 +516,8 @@ func TestBulkUpdateJobs(t *testing.T) { body: types.Job{Name: "updated-name"}, }, mockEndpoints: getJobsBulkUpdateEndpoints( + jobsResolveIDsQuery, + jobsResolveIDsResponseJSON, expectedBulkUpdateJobRequestJSON, http.StatusOK, jobsBulkUpdateResponseJSON, @@ -507,6 +536,8 @@ func TestBulkUpdateJobs(t *testing.T) { body: types.Job{Name: "updated-name"}, }, mockEndpoints: getJobsBulkUpdateEndpoints( + jobsResolveIDsQuery, + jobsResolveIDsResponseJSON, expectedBulkUpdateJobRequestJSON, http.StatusUnauthorized, "unauthorized", @@ -524,6 +555,8 @@ func TestBulkUpdateJobs(t *testing.T) { body: types.Job{Name: "updated-name"}, }, mockEndpoints: getJobsBulkUpdateEndpoints( + jobsResolveIDsQuery, + jobsResolveIDsResponseJSON, expectedBulkUpdateJobRequestJSON, http.StatusOK, "[", @@ -534,6 +567,26 @@ func TestBulkUpdateJobs(t *testing.T) { "\t* unexpected EOF", }, }, + { + name: "bulk update jobs not found", + request: bulkupdateRequest[types.Job]{ + ids: []string{"missing"}, + body: types.Job{Name: "updated-name"}, + }, + mockEndpoints: getJobsBulkUpdateEndpoints( + `or=(name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: bulkupdateExpected[types.Jobs]{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.JobType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -567,15 +620,19 @@ func TestGetJobs(t *testing.T) { }, }, { - name: "get jobs success with ids query", - request: getRequest{ids: []string{"id-a"}}, + name: "get jobs success with mixed ids, names query", + request: getRequest{ + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "job-name"}, + sort: "id.asc", + }, mockEndpoints: getJobsGetEndpoints( - "id=in.(id-a)", + "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ + "name.in.(123e4567-e89b-12d3-a456-426614174000,job-name))&order=id.asc", http.StatusOK, - jobsGetTwoResponseJSON, + jobsGetMixedResponseJSON, ), expected: getExpected[types.Jobs]{ - result: expectedResponseTwoJobs, + result: expectedResponseMixedJobs, }, }, { @@ -666,14 +723,14 @@ func TestGetJobs(t *testing.T) { name: "get jobs invalid json response", request: getRequest{ids: []string{"id-a"}}, mockEndpoints: getJobsGetEndpoints( - "id=in.(id-a)", + "or=(name.in.(id-a))", http.StatusOK, "[", ), expected: getExpected[types.Jobs]{ // jobs do not have controller ids so this query is for `id` // even though it is not a uuid. - err: "GET ${URL}/postgrest/jobs?id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/jobs?or=%28name.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF", }, @@ -682,12 +739,12 @@ func TestGetJobs(t *testing.T) { name: "get jobs non-200 from server", request: getRequest{ids: []string{"id-a"}}, mockEndpoints: getJobsGetEndpoints( - "id=in.(id-a)", + "or=(name.in.(id-a))", http.StatusUnauthorized, "unauthorized", ), expected: getExpected[types.Jobs]{ - err: "GET ${URL}/postgrest/jobs?id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/jobs?or=%28name.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -852,12 +909,14 @@ func TestDescribeJobs(t *testing.T) { } func getJobsDeleteEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getJobsGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCJobBulkDelete), @@ -868,8 +927,7 @@ func getJobsDeleteEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getJobsCreateEndpoints( @@ -894,12 +952,14 @@ func getJobsCreateEndpoints( } func getJobsBulkUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getJobsGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCJobBulkUpdate), @@ -910,8 +970,7 @@ func getJobsBulkUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getJobsGetEndpoints( diff --git a/internal/services/workbench/snapshots.go b/internal/services/workbench/snapshots.go index a778dcd..1debda5 100644 --- a/internal/services/workbench/snapshots.go +++ b/internal/services/workbench/snapshots.go @@ -22,29 +22,47 @@ import ( ) // GetSnapshots gets the snapshots in the given WorkbenchService. If given any -// ids, only the matching resources will be returned. Otherwise, all resources +// ids/names, only the matching resources will be returned. Otherwise, all resources // are returned. +// nolint:dupl func GetSnapshots( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.Snapshots, error) { - logutil.GetLogger().Info("getting snapshots", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting snapshots", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, constants.SortKeySource: {constants.SortKeySpecSourcePVCName}, constants.SortKeySize: {constants.SortKeyVolumeStatusRestoreSize}, constants.SortKeyReady: {constants.SortKeyVolumeStatusReadyToUse}, }) - return result.Split(Get[types.Snapshots]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointSnapshots), IDsOrControllerIDsQuery(ids, sortSpec), + snapshots, err := result.Split(Get[types.Snapshots]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointSnapshots), IDsControllerIDsOrNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + snapshots, + ), nil +} + +func resolveSnapshotIDs(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + return resolveIDs( + ctx, s, args, GetSnapshots, types.Snapshot.GetID, constants.SnapshotType, + ) } -// BulkDeleteSnapshots deletes the snapshot with the given id. -func BulkDeleteSnapshots(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { - logutil.GetLogger().Info("deleting snapshots", "ids", ids) +// BulkDeleteSnapshots deletes the snapshots with the given ids/names. +func BulkDeleteSnapshots(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + logutil.GetLogger().Info("deleting snapshots", "args", args) + ids, err := resolveSnapshotIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[[]string]( ctx, s, @@ -69,14 +87,18 @@ func CreateSnapshot( } // BulkUpdateSnapshots applies the given types.Snapshot to all the snapshots -// with the given ids. +// with the given ids/names. func BulkUpdateSnapshots( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, body types.Snapshot, ) (types.Snapshots, error) { - logutil.GetLogger().Info("updating snapshots", "ids", ids, "body", body) + logutil.GetLogger().Info("updating snapshots", "args", args, "body", body) + ids, err := resolveSnapshotIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[types.Snapshots]( ctx, s, diff --git a/internal/services/workbench/snapshots_test.go b/internal/services/workbench/snapshots_test.go index 3311d41..e032127 100644 --- a/internal/services/workbench/snapshots_test.go +++ b/internal/services/workbench/snapshots_test.go @@ -6,13 +6,13 @@ package workbench_test import ( "context" "encoding/json" - "fmt" "net/http" "strings" "testing" "time" "github.com/sassoftware/wbctl/internal/constants" + "github.com/sassoftware/wbctl/internal/i18n" "github.com/sassoftware/wbctl/internal/postgrest" "github.com/sassoftware/wbctl/internal/services/workbench" "github.com/sassoftware/wbctl/internal/stubs" @@ -51,6 +51,13 @@ const ( "name": "snapshot-controller", "username": "bob", "description": "controller" + }, + { + "id": "snap-name", + "controller_id": "ctrl-name", + "name": "snapshot-name", + "username": "carol", + "description": "name" } ] ` @@ -333,6 +340,9 @@ Volume Status: Content Name:Creation Time: ` + snapshotResolveIDsQuery = "or=(controller_id.in.(snap-1,snap-2),name.in.(snap-1,snap-2))" + snapshotResolveIDsResponseJSON = `[{"id":"snap-1"},{"id":"snap-2"}]` + snapshotDeleteSuccessResponseJSON = `["snap-1", "snap-2"]` expectedDeleteSnapshotRequestJSON = `{"ids":["snap-1","snap-2"]}` createSnapshotOneResponseBodyJSON = `{"id":"snap-1","name":"snapshot-1"}` expectedCreateSnapshotRequestJSON = `{"param":{"name":"snapshot-1"}}` @@ -350,7 +360,7 @@ var ( }, } - expectedResponseTwoSnapshots = types.Snapshots{ + expectedResponseMixedSnapshots = types.Snapshots{ { ID: "123e4567-e89b-12d3-a456-426614174000", ControllerID: "ctrl-uuid", @@ -365,6 +375,13 @@ var ( Username: "bob", Description: "controller", }, + { + ID: "snap-name", + ControllerID: "ctrl-name", + Name: "snapshot-name", + Username: "carol", + Description: "name", + }, } ) @@ -376,9 +393,11 @@ func TestDeleteSnapshot(t *testing.T) { ids: []string{"snap-1", "snap-2"}, }, mockEndpoints: getSnapshotDeleteEndpoints( + snapshotResolveIDsQuery, + snapshotResolveIDsResponseJSON, expectedDeleteSnapshotRequestJSON, http.StatusOK, - `["snap-1","snap-2"]`, + snapshotDeleteSuccessResponseJSON, ), expected: deleteExpected{ result: []string{"snap-1", "snap-2"}, @@ -390,6 +409,8 @@ func TestDeleteSnapshot(t *testing.T) { ids: []string{"snap-1", "snap-2"}, }, mockEndpoints: getSnapshotDeleteEndpoints( + snapshotResolveIDsQuery, + snapshotResolveIDsResponseJSON, expectedDeleteSnapshotRequestJSON, http.StatusUnauthorized, `unauthorized`, @@ -400,6 +421,25 @@ func TestDeleteSnapshot(t *testing.T) { "\t* 401 unauthorized", }, }, + { + name: "delete snapshot not found", + request: deleteRequest{ + ids: []string{"missing"}, + }, + mockEndpoints: getSnapshotDeleteEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: deleteExpected{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.SnapshotType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { @@ -486,6 +526,8 @@ func TestBulkUpdateSnapshots(t *testing.T) { body: types.Snapshot{Name: "updated-name"}, }, mockEndpoints: getSnapshotBulkUpdateEndpoints( + snapshotResolveIDsQuery, + snapshotResolveIDsResponseJSON, expectedBulkUpdateSnapshotRequestJSON, http.StatusOK, snapshotsBulkUpdateResponseJSON, @@ -504,6 +546,8 @@ func TestBulkUpdateSnapshots(t *testing.T) { body: types.Snapshot{Name: "updated-name"}, }, mockEndpoints: getSnapshotBulkUpdateEndpoints( + snapshotResolveIDsQuery, + snapshotResolveIDsResponseJSON, expectedBulkUpdateSnapshotRequestJSON, http.StatusUnauthorized, `unauthorized`, @@ -521,6 +565,8 @@ func TestBulkUpdateSnapshots(t *testing.T) { body: types.Snapshot{Name: "updated-name"}, }, mockEndpoints: getSnapshotBulkUpdateEndpoints( + snapshotResolveIDsQuery, + snapshotResolveIDsResponseJSON, expectedBulkUpdateSnapshotRequestJSON, http.StatusOK, `[`, @@ -531,6 +577,26 @@ func TestBulkUpdateSnapshots(t *testing.T) { "\t* unexpected EOF", }, }, + { + name: "bulk update snapshots not found", + request: bulkupdateRequest[types.Snapshot]{ + ids: []string{"missing"}, + body: types.Snapshot{Name: "updated-name"}, + }, + mockEndpoints: getSnapshotBulkUpdateEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: bulkupdateExpected[types.Snapshots]{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.SnapshotType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -564,19 +630,20 @@ func TestGetSnapshots(t *testing.T) { }, }, { - name: "get snapshots success with mixed uuid and controller ids query", + name: "get snapshots success with mixed uuid, controller id, names query", request: getRequest{ - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-snapshot-2"}, + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-snapshot-2", "snapshot-name"}, sort: "id.asc", }, mockEndpoints: getSnapshotsGetEndpoints( "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ - "controller_id.in.(controller-snapshot-2))&order=id.asc", + "controller_id.in.(controller-snapshot-2,snapshot-name),"+ + "name.in.(123e4567-e89b-12d3-a456-426614174000,controller-snapshot-2,snapshot-name))&order=id.asc", http.StatusOK, snapshotsGetMixedIDsResponseJSON, ), expected: getExpected[types.Snapshots]{ - result: expectedResponseTwoSnapshots, + result: expectedResponseMixedSnapshots, }, }, { @@ -712,12 +779,12 @@ func TestGetSnapshots(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getSnapshotsGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusOK, `[`, ), expected: getExpected[types.Snapshots]{ - err: "GET %s/postgrest/snapshots?controller_id=in.%%28id-a%%29 failed\n" + + err: "GET ${URL}/postgrest/snapshots?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF", }, @@ -728,12 +795,12 @@ func TestGetSnapshots(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getSnapshotsGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusUnauthorized, `unauthorized`, ), expected: getExpected[types.Snapshots]{ - err: "GET %s/postgrest/snapshots?controller_id=in.%%28id-a%%29 failed\n" + + err: "GET ${URL}/postgrest/snapshots?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -748,7 +815,7 @@ func TestGetSnapshots(t *testing.T) { actual, err := workbench.GetSnapshots(context.Background(), svc, tt.request.ids, tt.request.sort) if tt.expected.err != "" { - require.EqualError(t, err, fmt.Sprintf(tt.expected.err, srv.URL)) + require.EqualError(t, err, strings.ReplaceAll(tt.expected.err, "${URL}", srv.URL)) return } require.NoError(t, err) @@ -872,12 +939,14 @@ func TestDescribeSnapshots(t *testing.T) { } func getSnapshotDeleteEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getSnapshotsGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCSnapshotBulkDelete), @@ -888,8 +957,7 @@ func getSnapshotDeleteEndpoints( Body: responseBody, Status: responseStatus, }, - }, - } + }) } func getSnapshotCreateEndpoints( @@ -914,12 +982,14 @@ func getSnapshotCreateEndpoints( } func getSnapshotBulkUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getSnapshotsGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCSnapshotBulkUpdate), @@ -930,8 +1000,7 @@ func getSnapshotBulkUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getSnapshotsGetEndpoints( diff --git a/internal/services/workbench/util.go b/internal/services/workbench/util.go index 07e8b8d..497a548 100644 --- a/internal/services/workbench/util.go +++ b/internal/services/workbench/util.go @@ -34,42 +34,144 @@ func IDsQuery(ids []string, sortSpec string) url.Values { return query } -// IDsOrControllerIDsQuery returns a PostgREST compatible query for the given -// IDs and sort spec that will match either the actual id or the controller_id +// IDsControllerIDsOrNamesQuery returns a PostgREST compatible query for the +// given IDs/names and sort spec that will match the id, controller_id, or name // columns. -func IDsOrControllerIDsQuery(ids []string, sortSpec string) url.Values { +func IDsControllerIDsOrNamesQuery(args []string, sortSpec string) url.Values { + return buildFilterQuery(args, "controller_id", "name", sortSpec) +} + +// IDsOrNamesQuery returns a PostgREST compatible query for the given IDs/names +// and sort spec that will match the id or name columns. +func IDsOrNamesQuery(args []string, sortSpec string) url.Values { + return buildFilterQuery(args, "", "name", sortSpec) +} + +// IDsControllerIDsOrJobNamesQuery returns a PostgREST compatible query for the +// given IDs/names and sort spec that will match the id, controller_id, or job_name +// columns. +func IDsControllerIDsOrJobNamesQuery(args []string, sortSpec string) url.Values { + return buildFilterQuery(args, "controller_id", "job_name", sortSpec) +} + +// buildFilterQuery generates the query for the given IDs/names. UUID values +// are matched against the id column, while other values are matched against +// the controllerIDColumn if provided. Every value is matched against the +// nameColumn. +func buildFilterQuery(args []string, controllerIDColumn, nameColumn, sortSpec string) url.Values { query := url.Values{} if sortSpec != "" { query["order"] = []string{sortSpec} } + if len(args) == 0 { + return query + } uuidIDs := []string{} - controllerIDs := []string{} - for _, id := range ids { - if isUUID(id) { - uuidIDs = append(uuidIDs, id) + nonUUIDs := []string{} + for _, arg := range args { + if isUUID(arg) { + uuidIDs = append(uuidIDs, arg) } else { - controllerIDs = append(controllerIDs, id) + nonUUIDs = append(nonUUIDs, arg) } } - switch { - case len(uuidIDs) > 0 && len(controllerIDs) > 0: - query["or"] = []string{ - "(id.in.(" + strings.Join( - uuidIDs, - ",", - ) + "),controller_id.in.(" + strings.Join( - controllerIDs, - ",", - ) + "))", - } - case len(uuidIDs) > 0: - query["id"] = []string{"in.(" + strings.Join(uuidIDs, ",") + ")"} - case len(controllerIDs) > 0: - query["controller_id"] = []string{"in.(" + strings.Join(controllerIDs, ",") + ")"} + clauses := []string{} + if len(uuidIDs) > 0 { + clauses = append(clauses, "id.in.("+strings.Join(uuidIDs, ",")+")") } + if controllerIDColumn != "" && len(nonUUIDs) > 0 { + clauses = append(clauses, controllerIDColumn+".in.("+strings.Join(nonUUIDs, ",")+")") + } + clauses = append(clauses, nameColumn+".in.("+strings.Join(args, ",")+")") + query["or"] = []string{"(" + strings.Join(clauses, ",") + ")"} return query } +// selectByIDThenName narrows resources to those requested by args. It provides precedence +// for selecting by ID > Name. Each arg is resolved independently and no resource is +// returned twice. +func selectByIDThenName[T types.IdentifiableResource, TS ~[]T]( + args []string, + resources TS, +) TS { + if len(args) == 0 { + return resources + } + selected := make([]bool, len(resources)) + for _, arg := range args { + if arg == "" { + continue + } + for _, i := range matchIndexesByIDThenName(arg, resources) { + selected[i] = true + } + } + filtered := make(TS, 0, len(resources)) + for i, resource := range resources { + if selected[i] { + filtered = append(filtered, resource) + } + } + return filtered +} + +// matchIndexesByIDThenName returns the indexes of the resources matching args. +// Resources matching by ID or controller ID take precedence over those +// matching by name. +func matchIndexesByIDThenName[T types.IdentifiableResource, TS ~[]T]( + arg string, + resources TS, +) []int { + var byID, byName []int + for i, resource := range resources { + switch { + case resource.GetID() == arg || resource.GetControllerID() == arg: + byID = append(byID, i) + case resource.GetName() == arg: + byName = append(byName, i) + } + } + if len(byID) > 0 { + return byID + } + return byName +} + +// resolveIDs converts the given args, which may be ids, controller_ids, or names, +// into the database ids of the matching resources. +func resolveIDs[T any, TS ~[]T]( + ctx context.Context, + s *WorkbenchService, + args []string, + getter func(context.Context, *WorkbenchService, []string, string) (TS, error), + getIDFunc func(T) string, + resourceType string, +) ([]string, error) { + if len(args) == 0 { + return args, nil + } + resources, err := getter(ctx, s, args, "") + if err != nil { + return nil, err + } + if len(resources) == 0 { + return nil, errors.New( + i18n.Format( + i18n.ResourceNotFoundWithID, + i18n.A{ + constants.I18nArgTypeKey: resourceType, + constants.I18nArgKeyID: strings.Join(args, ", "), + }, + ), + ) + } + resolved := make([]string, 0, len(resources)) + for _, resource := range resources { + resolved = append(resolved, getIDFunc(resource)) + } + return resolved, nil +} + // Auth0IDsQuery returns an Auth0 compatible query for the given IDs. func Auth0IDsQuery(ids []string, sortSpec string) url.Values { if len(ids) == 0 && sortSpec == "" { diff --git a/internal/services/workbench/util_internal_test.go b/internal/services/workbench/util_internal_test.go new file mode 100644 index 0000000..7643536 --- /dev/null +++ b/internal/services/workbench/util_internal_test.go @@ -0,0 +1,180 @@ +// Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package workbench + +import ( + "context" + "errors" + "testing" + + "github.com/sassoftware/wbctl/internal/constants" + "github.com/sassoftware/wbctl/internal/i18n" + "github.com/stretchr/testify/require" +) + +type testResource struct { + ID string + ControllerID string + Name string +} + +type testResources []testResource + +func (r testResource) GetID() string { return r.ID } +func (r testResource) GetControllerID() string { return r.ControllerID } +func (r testResource) GetName() string { return r.Name } + +func TestSelectByIDThenName(t *testing.T) { + a := testResource{ID: "id-a", ControllerID: "ctrl-a", Name: "name-a"} + b := testResource{ID: "id-b", ControllerID: "ctrl-b", Name: "name-b"} + // c has a name equal to a's ID, to exercise ID > name precedence. + c := testResource{ID: "id-c", ControllerID: "ctrl-c", Name: "id-a"} + // d shares a name with b. + d := testResource{ID: "id-d", ControllerID: "ctrl-d", Name: "name-b"} + + tests := []struct { + name string + args []string + resources testResources + expected testResources + }{ + { + name: "no args returns all resources", + args: nil, + resources: testResources{a, b}, + expected: testResources{a, b}, + }, + { + name: "empty string args are skipped", + args: []string{"", "id-a"}, + resources: testResources{a, b}, + expected: testResources{a}, + }, + { + name: "match by id", + args: []string{"id-a"}, + resources: testResources{a, b}, + expected: testResources{a}, + }, + { + name: "match by controller id", + args: []string{"ctrl-b"}, + resources: testResources{a, b}, + expected: testResources{b}, + }, + { + name: "match by name", + args: []string{"name-a"}, + resources: testResources{a, b}, + expected: testResources{a}, + }, + { + name: "id takes precedence over name", + args: []string{"id-a"}, + resources: testResources{a, c}, + expected: testResources{a}, + }, + { + name: "name used when no id matches", + args: []string{"name-b"}, + resources: testResources{a, b, d}, + expected: testResources{b, d}, + }, + { + name: "no match returns empty", + args: []string{"missing"}, + resources: testResources{a, b}, + expected: testResources{}, + }, + { + name: "multiple args are resolved independently", + args: []string{"id-a", "name-b"}, + resources: testResources{a, b, c, d}, + expected: testResources{a, b, d}, + }, + { + name: "resource matched by several args is returned once", + args: []string{"id-a", "name-a", "ctrl-a"}, + resources: testResources{a, b}, + expected: testResources{a}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := selectByIDThenName( + tt.args, + tt.resources, + ) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestResolveIDs(t *testing.T) { + tests := []struct { + name string + args []string + getter func(context.Context, *WorkbenchService, []string, string) (testResources, error) + expected []string + expectedErr string + }{ + { + name: "empty args returns args without calling getter", + args: nil, + getter: func(context.Context, *WorkbenchService, []string, string) (testResources, error) { + t.Error("getter must not be called") + return nil, nil + }, + expected: nil, + }, + { + name: "resolves to ids", + args: []string{"name-a", "ctrl-b"}, + getter: func(_ context.Context, _ *WorkbenchService, args []string, sortSpec string) (testResources, error) { + require.Equal(t, []string{"name-a", "ctrl-b"}, args) + require.Empty(t, sortSpec) + return testResources{{ID: "id-a"}, {ID: "id-b"}}, nil + }, + expected: []string{"id-a", "id-b"}, + }, + { + name: "getter error is returned", + args: []string{"name-a"}, + getter: func(context.Context, *WorkbenchService, []string, string) (testResources, error) { + return nil, errors.New("getter err") + }, + expectedErr: "getter err", + }, + { + name: "no matches returns not found error", + args: []string{"ws-1", "ws-2"}, + getter: func(context.Context, *WorkbenchService, []string, string) (testResources, error) { + return testResources{}, nil + }, + expectedErr: i18n.Format( + i18n.ResourceNotFoundWithID, + i18n.A{ + constants.I18nArgTypeKey: constants.VolumeType, + constants.I18nArgKeyID: "ws-1, ws-2", + }, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := resolveIDs( + context.Background(), nil, tt.args, tt.getter, testResource.GetID, constants.VolumeType, + ) + if tt.expectedErr != "" { + require.EqualError(t, err, tt.expectedErr) + require.Nil(t, actual) + return + } + require.NoError(t, err) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/internal/services/workbench/util_test.go b/internal/services/workbench/util_test.go index e1de8da..dfb97f8 100644 --- a/internal/services/workbench/util_test.go +++ b/internal/services/workbench/util_test.go @@ -11,7 +11,39 @@ import ( "github.com/stretchr/testify/require" ) -func TestIDsOrControllerIDsQuery(t *testing.T) { +func TestIDsQuery(t *testing.T) { + tests := []struct { + name string + ids []string + expected url.Values + sort string + }{ + { + name: "empty ids", + ids: nil, + expected: url.Values{}, + }, + { + name: "single id", + ids: []string{"id-a"}, + expected: url.Values{"id": []string{"in.(id-a)"}}, + }, + { + name: "multiple ids", + ids: []string{"id-a", "id-b"}, + expected: url.Values{"id": []string{"in.(id-a,id-b)"}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := workbench.IDsQuery(tt.ids, tt.sort) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestIDsControllerIDsOrNamesQuery(t *testing.T) { tests := []struct { name string ids []string @@ -28,38 +60,100 @@ func TestIDsOrControllerIDsQuery(t *testing.T) { ids: []string{"123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001"}, sort: "id.asc", expected: url.Values{ - "id": []string{"in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001)"}, + "or": []string{ + "(id.in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001)," + + "name.in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001))", + }, "order": []string{"id.asc"}, }, }, { - name: "all controller ids", - ids: []string{"controller-a", "controller-b"}, - expected: url.Values{"controller_id": []string{"in.(controller-a,controller-b)"}}, + name: "all controller ids / names", + ids: []string{"controller-a", "name-a"}, + expected: url.Values{ + "or": []string{ + "(controller_id.in.(controller-a,name-a),name.in.(controller-a,name-a))", + }, + }, }, { - name: "mixed uuid and controller ids", - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-a"}, + name: "mixed uuid, controller id, names", + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-a", "name-a"}, expected: url.Values{ - "or": []string{"(id.in.(123e4567-e89b-12d3-a456-426614174000),controller_id.in.(controller-a))"}, + "or": []string{ + "(id.in.(123e4567-e89b-12d3-a456-426614174000)," + + "controller_id.in.(controller-a,name-a)," + + "name.in.(123e4567-e89b-12d3-a456-426614174000,controller-a,name-a))", + }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := workbench.IDsOrControllerIDsQuery(tt.ids, tt.sort) + actual := workbench.IDsControllerIDsOrNamesQuery(tt.ids, tt.sort) require.Equal(t, tt.expected, actual) }) } } -func TestIDsQuery(t *testing.T) { +func TestIDsOrNamesQuery(t *testing.T) { tests := []struct { name string ids []string + sort string expected url.Values + }{ + { + name: "empty ids", + ids: nil, + expected: url.Values{}, + }, + { + name: "all uuids", + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001"}, + sort: "id.asc", + expected: url.Values{ + "or": []string{ + "(id.in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001)," + + "name.in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001))", + }, + "order": []string{"id.asc"}, + }, + }, + { + name: "all names", + ids: []string{"name-a", "name-b"}, + expected: url.Values{ + "or": []string{"(name.in.(name-a,name-b))"}, + }, + }, + { + name: "mixed uuid and names", + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "name-a"}, + expected: url.Values{ + "or": []string{ + "(id.in.(123e4567-e89b-12d3-a456-426614174000)," + + "name.in.(123e4567-e89b-12d3-a456-426614174000,name-a))", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := workbench.IDsOrNamesQuery(tt.ids, tt.sort) + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestIDsControllerIDsOrJobNamesQuery(t *testing.T) { + tests := []struct { + name string + ids []string sort string + expected url.Values }{ { name: "empty ids", @@ -67,20 +161,42 @@ func TestIDsQuery(t *testing.T) { expected: url.Values{}, }, { - name: "single id", - ids: []string{"id-a"}, - expected: url.Values{"id": []string{"in.(id-a)"}}, + name: "all uuids", + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001"}, + sort: "id.asc", + expected: url.Values{ + "or": []string{ + "(id.in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001)," + + "job_name.in.(123e4567-e89b-12d3-a456-426614174000,123e4567-e89b-12d3-a456-426614174001))", + }, + "order": []string{"id.asc"}, + }, }, { - name: "multiple ids", - ids: []string{"id-a", "id-b"}, - expected: url.Values{"id": []string{"in.(id-a,id-b)"}}, + name: "all controller ids / job names", + ids: []string{"controller-a", "job-a"}, + expected: url.Values{ + "or": []string{ + "(controller_id.in.(controller-a,job-a),job_name.in.(controller-a,job-a))", + }, + }, + }, + { + name: "mixed uuid, controller id, job names", + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-a", "job-a"}, + expected: url.Values{ + "or": []string{ + "(id.in.(123e4567-e89b-12d3-a456-426614174000)," + + "controller_id.in.(controller-a,job-a)," + + "job_name.in.(123e4567-e89b-12d3-a456-426614174000,controller-a,job-a))", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := workbench.IDsQuery(tt.ids, tt.sort) + actual := workbench.IDsControllerIDsOrJobNamesQuery(tt.ids, tt.sort) require.Equal(t, tt.expected, actual) }) } diff --git a/internal/services/workbench/workbench.go b/internal/services/workbench/workbench.go index 05259db..99eddd0 100644 --- a/internal/services/workbench/workbench.go +++ b/internal/services/workbench/workbench.go @@ -17,6 +17,7 @@ import ( "github.com/sassoftware/wbctl/internal/httputil" "github.com/sassoftware/wbctl/internal/i18n" "github.com/sassoftware/wbctl/internal/maputil" + "github.com/sassoftware/wbctl/internal/sliceutil" "github.com/sassoftware/wbctl/internal/types" ) @@ -127,7 +128,7 @@ func GetValidDataplaneID(ctx context.Context, svc *WorkbenchService, dataplaneID return dataplanes[0].ID, nil } -// GetValidWorkspaceRequests takes the given worksaceIDs, fetches those +// GetValidWorkspaceRequests takes the given workspaceIDs, fetches those // workspaces from the server, and returns the database id and full workspace // request body for each. func GetValidWorkspaceRequests( @@ -149,12 +150,16 @@ func GetValidWorkspaceRequests( ), ) } - id := workspaces[0].ControllerID + workspace, err := sliceutil.GetAndEnsureOnlyOne(workspaces) + if err != nil { + return nil, err + } + id := workspace.ControllerID if id == "" { - id = workspaces[0].ID + id = workspace.ID } - mountName := workspaces[0].Name - homeDir := isHomeDir(workspaces[0]) + mountName := workspace.Name + homeDir := isHomeDir(workspace) if homeDir { mountName = "sas" } @@ -164,7 +169,7 @@ func GetValidWorkspaceRequests( MountName: mountName, }, HomeDir: homeDir, - DbID: workspaces[0].ID, + DbID: workspace.ID, }) } return validWorkspaces, nil diff --git a/internal/services/workbench/workbench_test.go b/internal/services/workbench/workbench_test.go index 0fd8e33..6dc414d 100644 --- a/internal/services/workbench/workbench_test.go +++ b/internal/services/workbench/workbench_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/sassoftware/wbctl/internal/constants" + "github.com/sassoftware/wbctl/internal/i18n" "github.com/sassoftware/wbctl/internal/postgrest" "github.com/sassoftware/wbctl/internal/services/workbench" "github.com/sassoftware/wbctl/internal/stubs" @@ -378,7 +379,7 @@ func TestGetWorkspaceRequests(t *testing.T) { request: request{workspaceIDs: []string{"ws-1"}}, mockEndpoints: getEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), - "controller_id=in.(ws-1)", + "or=(controller_id.in.(ws-1),name.in.(ws-1))", http.StatusOK, `[ {"id":"ws-1","controller_id":"ctrl-1","name":"myws","properties":{"HOME_DIRECTORY":"true"}} ]`, ), @@ -393,7 +394,7 @@ func TestGetWorkspaceRequests(t *testing.T) { request: request{workspaceIDs: []string{"00000000-0000-0000-0000-000000000001"}}, mockEndpoints: getEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), - "id=in.(00000000-0000-0000-0000-000000000001)", + "or=(id.in.(00000000-0000-0000-0000-000000000001),name.in.(00000000-0000-0000-0000-000000000001))", http.StatusOK, `[ {"id":"00000000-0000-0000-0000-000000000001","controller_id":"","name":"wsname","properties":{}} ]`, ), @@ -408,23 +409,36 @@ func TestGetWorkspaceRequests(t *testing.T) { request: request{workspaceIDs: []string{"missing"}}, mockEndpoints: getEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), - "controller_id=in.(missing)", + "or=(controller_id.in.(missing),name.in.(missing))", http.StatusOK, `[]`, ), expected: expected{err: "volume not found: missing"}, }, + { + name: "multiple workspaces match one name", + request: request{workspaceIDs: []string{"ws-name"}}, + mockEndpoints: getEndpoints( + postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), + "or=(controller_id.in.(ws-name),name.in.(ws-name))", + http.StatusOK, + `[ {"id":"ws-1","controller_id":"ctrl-a","name":"ws-name","properties":{}},`+ + `{"id":"ws-2","controller_id":"ctrl-b","name":"ws-name","properties":{}} ]`, + ), + expected: expected{err: i18n.Format(i18n.MultipleElementsFound)}, + }, { name: "invalid workspace response", request: request{workspaceIDs: []string{"ws-1"}}, mockEndpoints: getEndpoints( postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), - "controller_id=in.(ws-1)", + "or=(controller_id.in.(ws-1),name.in.(ws-1))", http.StatusOK, `[invalid`, ), expected: expected{ - err: "GET ${URL}/postgrest/workspaces?controller_id=in.%28ws-1%29 failed" + + err: "GET ${URL}/postgrest/workspaces?" + + "or=%28controller_id.in.%28ws-1%29%2Cname.in.%28ws-1%29%29 failed" + "\n Caused by:" + "\n\t* invalid character 'i' looking for beginning of value", }, diff --git a/internal/services/workbench/workbenches.go b/internal/services/workbench/workbenches.go index 488359b..d232af2 100644 --- a/internal/services/workbench/workbenches.go +++ b/internal/services/workbench/workbenches.go @@ -20,27 +20,45 @@ import ( ) // GetWorkbenches gets the workbenches in the given WorkbenchService. If given -// any ids, only the matching resources will be returned. Otherwise, all +// any ids/names, only the matching resources will be returned. Otherwise, all // resources are returned. +// nolint:dupl func GetWorkbenches( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.Workbenches, error) { - logutil.GetLogger().Info("getting workbenches", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting workbenches", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, constants.SortKeyState: {constants.SortKeyCurrentState}, }) - return result.Split(Get[types.Workbenches]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), IDsOrControllerIDsQuery(ids, sortSpec), + workbenches, err := result.Split(Get[types.Workbenches]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointWorkbenches), IDsControllerIDsOrNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + workbenches, + ), nil +} + +func resolveWorkbenchIDs(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + return resolveIDs( + ctx, s, args, GetWorkbenches, types.Workbench.GetID, constants.WorkbenchType, + ) } -// BulkDeleteWorkbenches deletes the workbench with the given id. -func BulkDeleteWorkbenches(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { - logutil.GetLogger().Info("deleting workbenches", "ids", ids) +// BulkDeleteWorkbenches deletes the workbenches with the given ids/names. +func BulkDeleteWorkbenches(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + logutil.GetLogger().Info("deleting workbenches", "args", args) + ids, err := resolveWorkbenchIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[[]string]( ctx, s, @@ -64,15 +82,19 @@ func CreateWorkbench( )) } -// BulkUpdateWorkbenches applies the given types.Workbench to all the -// workbenches with the given ids. +// BulkUpdateWorkbenches applies the given types.Workbench to all the +// workbenches with the given ids/names. func BulkUpdateWorkbenches( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, body types.Workbench, ) (types.Workbenches, error) { - logutil.GetLogger().Info("updating workbenches", "ids", ids, "body", body) + logutil.GetLogger().Info("updating workbenches", "args", args, "body", body) + ids, err := resolveWorkbenchIDs(ctx, s, args) + if err != nil { + return nil, err + } return bulkUpdateForUniqueNameConstrainedResources[types.Workbench, types.Workbenches]( ctx, s, diff --git a/internal/services/workbench/workbenches_test.go b/internal/services/workbench/workbenches_test.go index 211d223..5be7103 100644 --- a/internal/services/workbench/workbenches_test.go +++ b/internal/services/workbench/workbenches_test.go @@ -6,7 +6,6 @@ package workbench_test import ( "context" "encoding/json" - "fmt" "net/http" "strings" "testing" @@ -55,6 +54,13 @@ const ( "name": "wb-controller", "username": "bob", "application_id": "app-2" + }, + { + "id": "wb-name", + "controller_id": "ctrl-name", + "name": "workbench-name", + "username": "carol", + "application_id": "app-3" } ] ` @@ -323,6 +329,9 @@ Properties: "POST ${URL}/postgrest/rpc/workbench_update failed" + "\n Caused by:" + "\n\t* invalid character 'i' looking for beginning of value" + workbenchResolveIDsQuery = "or=(controller_id.in.(wb-1,wb-2),name.in.(wb-1,wb-2))" + workbenchResolveIDsResponseJSON = `[{"id":"wb-1"},{"id":"wb-2"}]` + workbenchDeleteSuccessResponseJSON = `["wb-1", "wb-2"]` expectedDeleteWorkbenchRequestJSON = `{"ids":["wb-1","wb-2"]}` createWorkbenchResponseJSON = `{"id":"wb-1","name":"wb-one"}` expectedBulkUpdateWorkbenchRequestJSON = `{"ids":["wb-1","wb-2"],"workbench":{"desired_state":"STOPPED"}}` @@ -344,7 +353,7 @@ var ( }, } - expectedResponseTwoWorkbenches = types.Workbenches{ + expectedResponseMixedWorkbenches = types.Workbenches{ { ID: "123e4567-e89b-12d3-a456-426614174000", ControllerID: "ctrl-uuid", @@ -359,6 +368,13 @@ var ( Username: "bob", ApplicationID: "app-2", }, + { + ID: "wb-name", + ControllerID: "ctrl-name", + Name: "workbench-name", + Username: "carol", + ApplicationID: "app-3", + }, } ) @@ -370,9 +386,11 @@ func TestDeleteWorkbench(t *testing.T) { ids: []string{"wb-1", "wb-2"}, }, mockEndpoints: getWorkbenchesDeleteEndpoints( + workbenchResolveIDsQuery, + workbenchResolveIDsResponseJSON, expectedDeleteWorkbenchRequestJSON, http.StatusOK, - `["wb-1", "wb-2"]`, + workbenchDeleteSuccessResponseJSON, ), expected: deleteExpected{ result: []string{"wb-1", "wb-2"}, @@ -384,16 +402,37 @@ func TestDeleteWorkbench(t *testing.T) { ids: []string{"wb-1", "wb-2"}, }, mockEndpoints: getWorkbenchesDeleteEndpoints( + workbenchResolveIDsQuery, + workbenchResolveIDsResponseJSON, expectedDeleteWorkbenchRequestJSON, http.StatusUnauthorized, "unauthorized", ), expected: deleteExpected{ - err: "POST %s/postgrest/rpc/workbench_bulk_delete failed\n" + + err: "POST ${URL}/postgrest/rpc/workbench_bulk_delete failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, }, + { + name: "delete workbench not found", + request: deleteRequest{ + ids: []string{"missing"}, + }, + mockEndpoints: getWorkbenchesDeleteEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: deleteExpected{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.WorkbenchType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { @@ -403,7 +442,7 @@ func TestDeleteWorkbench(t *testing.T) { svc := workbench.NewWorkbenchService(srv.URL, "access-token", "csrf-token") actual, err := workbench.BulkDeleteWorkbenches(context.Background(), svc, tt.request.ids) if tt.expected.err != "" { - require.EqualError(t, err, fmt.Sprintf(tt.expected.err, srv.URL)) + require.EqualError(t, err, strings.ReplaceAll(tt.expected.err, "${URL}", srv.URL)) return } require.NoError(t, err) @@ -450,7 +489,7 @@ func TestCreateWorkbench(t *testing.T) { "unauthorized", ), expected: createExpected[types.Workbench]{ - err: "POST %s/postgrest/rpc/workbench_create failed\n" + + err: "POST ${URL}/postgrest/rpc/workbench_create failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -465,7 +504,7 @@ func TestCreateWorkbench(t *testing.T) { actual, err := workbench.CreateWorkbench(context.Background(), svc, tt.request.body) if tt.expected.err != "" { - require.EqualError(t, err, fmt.Sprintf(tt.expected.err, srv.URL)) + require.EqualError(t, err, strings.ReplaceAll(tt.expected.err, "${URL}", srv.URL)) return } require.NoError(t, err) @@ -483,6 +522,8 @@ func TestBulkUpdateWorkbenches(t *testing.T) { body: types.Workbench{DesiredState: "STOPPED"}, }, mockEndpoints: getWorkbenchesBulkUpdateEndpoints( + workbenchResolveIDsQuery, + workbenchResolveIDsResponseJSON, expectedBulkUpdateWorkbenchRequestJSON, http.StatusOK, workbenchesBulkUpdateResponseJSON, @@ -501,6 +542,8 @@ func TestBulkUpdateWorkbenches(t *testing.T) { body: types.Workbench{DesiredState: "STOPPED"}, }, mockEndpoints: getWorkbenchesBulkUpdateEndpoints( + workbenchResolveIDsQuery, + workbenchResolveIDsResponseJSON, expectedBulkUpdateWorkbenchRequestJSON, http.StatusUnauthorized, "unauthorized", @@ -518,6 +561,8 @@ func TestBulkUpdateWorkbenches(t *testing.T) { body: types.Workbench{DesiredState: "STOPPED"}, }, mockEndpoints: getWorkbenchesBulkUpdateEndpoints( + workbenchResolveIDsQuery, + workbenchResolveIDsResponseJSON, expectedBulkUpdateWorkbenchRequestJSON, http.StatusOK, "[", @@ -535,6 +580,8 @@ func TestBulkUpdateWorkbenches(t *testing.T) { body: types.Workbench{Name: "wb-one-renamed"}, }, mockEndpoints: getWorkbenchesUpdateEndpoints( + `or=(controller_id.in.(wb-1),name.in.(wb-1))`, + `[{"id":"wb-1"}]`, expectedUpdateWorkbenchRequest2JSON, http.StatusOK, `{"id":"wb-1","name":"wb-one-renamed"}`, @@ -549,11 +596,38 @@ func TestBulkUpdateWorkbenches(t *testing.T) { ids: []string{"wb-1", "wb-2"}, body: types.Workbench{Name: "wb-one-renamed"}, }, + mockEndpoints: getWorkbenchesBulkUpdateEndpoints( + workbenchResolveIDsQuery, + workbenchResolveIDsResponseJSON, + "", + http.StatusOK, + "", + ), expected: bulkupdateExpected[types.Workbenches]{ err: i18n.Format(i18n.BulkUpdateNameError, i18n.A{constants.I18nArgKeyType: constants.WorkbenchesType}), }, }, + { + name: "bulk update workbenches with no matching resources returns not found", + request: bulkupdateRequest[types.Workbench]{ + ids: []string{"missing"}, + body: types.Workbench{DesiredState: "STOPPED"}, + }, + mockEndpoints: getWorkbenchesBulkUpdateEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: bulkupdateExpected[types.Workbenches]{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.WorkbenchType, + constants.I18nArgKeyID: "missing", + }), + }, + }, { name: "update single workbench name invalid response", request: bulkupdateRequest[types.Workbench]{ @@ -561,6 +635,8 @@ func TestBulkUpdateWorkbenches(t *testing.T) { body: types.Workbench{Name: "UPDATED"}, }, mockEndpoints: getWorkbenchesUpdateEndpoints( + `or=(controller_id.in.(wb-1),name.in.(wb-1))`, + `[{"id":"wb-1"}]`, expectedUpdateWorkbenchRequestJSON, http.StatusOK, "[invalid", @@ -602,19 +678,20 @@ func TestGetWorkbenches(t *testing.T) { }, }, { - name: "get workbench success with mixed uuid and controller ids query", + name: "get workbench success with mixed uuid, controller ids, names query", request: getRequest{ - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-wb-2"}, + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-wb-2", "workbench-name"}, sort: "id.asc", }, mockEndpoints: getWorkbenchesGetEndpoints( "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ - "controller_id.in.(controller-wb-2))&order=id.asc", + "controller_id.in.(controller-wb-2,workbench-name),"+ + "name.in.(123e4567-e89b-12d3-a456-426614174000,controller-wb-2,workbench-name))&order=id.asc", http.StatusOK, workbenchesGetMixedIDsResponseJSON, ), expected: getExpected[types.Workbenches]{ - result: expectedResponseTwoWorkbenches, + result: expectedResponseMixedWorkbenches, }, }, { @@ -763,12 +840,12 @@ func TestGetWorkbenches(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getWorkbenchesGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusOK, "[", ), expected: getExpected[types.Workbenches]{ - err: "GET ${URL}/postgrest/workbenches?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/workbenches?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF", }, @@ -779,12 +856,12 @@ func TestGetWorkbenches(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getWorkbenchesGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusUnauthorized, "unauthorized", ), expected: getExpected[types.Workbenches]{ - err: "GET ${URL}/postgrest/workbenches?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/workbenches?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -922,12 +999,14 @@ func TestDescribeWorkbenches(t *testing.T) { } func getWorkbenchesDeleteEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getWorkbenchesGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchBulkDelete), @@ -938,8 +1017,7 @@ func getWorkbenchesDeleteEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getWorkbenchesCreateEndpoints( @@ -964,12 +1042,14 @@ func getWorkbenchesCreateEndpoints( } func getWorkbenchesBulkUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getWorkbenchesGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchBulkUpdate), @@ -980,17 +1060,18 @@ func getWorkbenchesBulkUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getWorkbenchesUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getWorkbenchesGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkbenchUpdate), @@ -1001,8 +1082,7 @@ func getWorkbenchesUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getWorkbenchesGetEndpoints( diff --git a/internal/services/workbench/workspaces.go b/internal/services/workbench/workspaces.go index ad46b7e..4941448 100644 --- a/internal/services/workbench/workspaces.go +++ b/internal/services/workbench/workspaces.go @@ -21,29 +21,47 @@ import ( ) // GetWorkspaces gets the workspaces in the given WorkbenchService. If given any -// ids, only the matching resources will be returned. Otherwise, all resources +// ids/names, only the matching resources will be returned. Otherwise, all resources // are returned. +// nolint:dupl func GetWorkspaces( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, sortSpec string, ) (types.Workspaces, error) { - logutil.GetLogger().Info("getting volumes", "ids", ids, "sort", sortSpec) + logutil.GetLogger().Info("getting volumes", "args", args, "sort", sortSpec) sortSpec = normalizeSortSpec(sortSpec, map[string][]string{ constants.SortKeyAge: {constants.SortKeyCreatedAt}, constants.SortKeySize: {constants.SortKeyStorageSizeG}, constants.SortKeyHomeDir: {constants.SortKeyPropertiesHomeDirectory}, constants.SortKeyStorageClassName: {constants.SortKeyStorageSpecStorageClassName}, }) - return result.Split(Get[types.Workspaces]( - ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), IDsOrControllerIDsQuery(ids, sortSpec), + workspaces, err := result.Split(Get[types.Workspaces]( + ctx, s, postgrest.Endpoint(constants.WorkbenchEndpointWorkspaces), IDsControllerIDsOrNamesQuery(args, sortSpec), )) + if err != nil { + return nil, err + } + return selectByIDThenName( + args, + workspaces, + ), nil +} + +func resolveWorkspaceIDs(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + return resolveIDs( + ctx, s, args, GetWorkspaces, types.Workspace.GetID, constants.VolumeType, + ) } -// BulkDeleteWorkspaces deletes the workspaces with the given ids. -func BulkDeleteWorkspaces(ctx context.Context, s *WorkbenchService, ids []string) ([]string, error) { - logutil.GetLogger().Info("deleting volumes", "ids", ids) +// BulkDeleteWorkspaces deletes the workspaces with the given ids/names. +func BulkDeleteWorkspaces(ctx context.Context, s *WorkbenchService, args []string) ([]string, error) { + logutil.GetLogger().Info("deleting volumes", "args", args) + ids, err := resolveWorkspaceIDs(ctx, s, args) + if err != nil { + return nil, err + } return result.Split(Post[[]string]( ctx, s, @@ -67,15 +85,19 @@ func CreateWorkspace( )) } -// BulkUpdateWorkspaces applies the given types.Workspace to all the workspaces -// with the given ids. +// BulkUpdateWorkspaces applies the given types.Workspace to all the +// workspaces with the given ids/names. func BulkUpdateWorkspaces( ctx context.Context, s *WorkbenchService, - ids []string, + args []string, body types.Workspace, ) (types.Workspaces, error) { - logutil.GetLogger().Info("updating volumes", "ids", ids) + logutil.GetLogger().Info("updating volumes", "args", args) + ids, err := resolveWorkspaceIDs(ctx, s, args) + if err != nil { + return nil, err + } return bulkUpdateForUniqueNameConstrainedResources[types.Workspace, types.Workspaces]( ctx, s, diff --git a/internal/services/workbench/workspaces_test.go b/internal/services/workbench/workspaces_test.go index 1f53a1d..b0490fe 100644 --- a/internal/services/workbench/workspaces_test.go +++ b/internal/services/workbench/workspaces_test.go @@ -50,6 +50,12 @@ const ( "controller_id": "controller-ws-2", "name": "workspace-controller", "username": "bob" + }, + { + "id": "ws-name", + "controller_id": "ctrl-name", + "name": "workspace-name", + "username": "carol" } ] ` @@ -299,6 +305,9 @@ Properties: } ] ` + workspaceResolveIDsQuery = "or=(controller_id.in.(ws-1,ws-2),name.in.(ws-1,ws-2))" + workspaceResolveIDsResponseJSON = `[{"id":"ws-1"},{"id":"ws-2"}]` + workspaceDeleteSuccessResponseJSON = `["ws-1", "ws-2"]` expectedDeleteWorkspaceRequestJSON = `{"ids":["ws-1", "ws-2"]}` expectedBulkUpdateWorkspaceRequestJSON = `{"ids":["ws-1","ws-2"],"workspace":` + `{"description":"updated-description","pvc_status":{"status":{}}}}` @@ -333,6 +342,12 @@ var ( Name: "workspace-controller", Username: "bob", }, + { + ID: "ws-name", + ControllerID: "ctrl-name", + Name: "workspace-name", + Username: "carol", + }, } ) @@ -344,9 +359,11 @@ func TestDeleteWorkspace(t *testing.T) { ids: []string{"ws-1", "ws-2"}, }, mockEndpoints: getWorkspaceDeleteEndpoints( + workspaceResolveIDsQuery, + workspaceResolveIDsResponseJSON, expectedDeleteWorkspaceRequestJSON, http.StatusOK, - `["ws-1", "ws-2"]`, + workspaceDeleteSuccessResponseJSON, ), expected: deleteExpected{ result: []string{"ws-1", "ws-2"}, @@ -358,6 +375,8 @@ func TestDeleteWorkspace(t *testing.T) { ids: []string{"ws-1", "ws-2"}, }, mockEndpoints: getWorkspaceDeleteEndpoints( + workspaceResolveIDsQuery, + workspaceResolveIDsResponseJSON, expectedDeleteWorkspaceRequestJSON, http.StatusUnauthorized, `unauthorized`, @@ -368,6 +387,25 @@ func TestDeleteWorkspace(t *testing.T) { "\t* 401 unauthorized", }, }, + { + name: "delete workspace not found", + request: deleteRequest{ + ids: []string{"missing"}, + }, + mockEndpoints: getWorkspaceDeleteEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: deleteExpected{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.VolumeType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { @@ -459,6 +497,8 @@ func TestBulkUpdateWorkspaces(t *testing.T) { body: types.Workspace{Description: "updated-description"}, }, mockEndpoints: getWorkspaceBulkUpdateEndpoints( + workspaceResolveIDsQuery, + workspaceResolveIDsResponseJSON, expectedBulkUpdateWorkspaceRequestJSON, http.StatusOK, workspacesBulkUpdateResponseJSON, @@ -477,6 +517,8 @@ func TestBulkUpdateWorkspaces(t *testing.T) { body: types.Workspace{Description: "updated-description"}, }, mockEndpoints: getWorkspaceBulkUpdateEndpoints( + workspaceResolveIDsQuery, + workspaceResolveIDsResponseJSON, expectedBulkUpdateWorkspaceRequestJSON, http.StatusUnauthorized, `unauthorized`, @@ -494,6 +536,8 @@ func TestBulkUpdateWorkspaces(t *testing.T) { body: types.Workspace{Description: "updated-description"}, }, mockEndpoints: getWorkspaceBulkUpdateEndpoints( + workspaceResolveIDsQuery, + workspaceResolveIDsResponseJSON, expectedBulkUpdateWorkspaceRequestJSON, http.StatusOK, `[`, @@ -511,6 +555,8 @@ func TestBulkUpdateWorkspaces(t *testing.T) { body: types.Workspace{Name: "workspace-one-renamed"}, }, mockEndpoints: getWorkspaceUpdateEndpoints( + `or=(controller_id.in.(ws-1),name.in.(ws-1))`, + `[{"id":"ws-1"}]`, expectedUpdateWorkspaceRequestJSON, http.StatusOK, createWorkspaceRenamedResponseJSON, @@ -525,11 +571,38 @@ func TestBulkUpdateWorkspaces(t *testing.T) { ids: []string{"ws-1", "ws-2"}, body: types.Workspace{Name: "workspace-one-renamed"}, }, + mockEndpoints: getWorkspaceBulkUpdateEndpoints( + workspaceResolveIDsQuery, + workspaceResolveIDsResponseJSON, + "", + http.StatusOK, + "", + ), expected: bulkupdateExpected[types.Workspaces]{ err: i18n.Format(i18n.BulkUpdateNameError, i18n.A{constants.I18nArgKeyType: constants.VolumesType}), }, }, + { + name: "bulk update workspaces with no matching resources returns not found", + request: bulkupdateRequest[types.Workspace]{ + ids: []string{"missing"}, + body: types.Workspace{Description: "updated-description"}, + }, + mockEndpoints: getWorkspaceBulkUpdateEndpoints( + `or=(controller_id.in.(missing),name.in.(missing))`, + `[]`, + "", + http.StatusOK, + "", + ), + expected: bulkupdateExpected[types.Workspaces]{ + err: i18n.Format(i18n.ResourceNotFoundWithID, i18n.A{ + constants.I18nArgTypeKey: constants.VolumeType, + constants.I18nArgKeyID: "missing", + }), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -564,14 +637,15 @@ func TestGetWorkspaces(t *testing.T) { }, }, { - name: "get workspaces success with mixed uuid and controller ids query", + name: "get workspaces success with mixed uuid, controller ids, names query", request: getRequest{ - ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-ws-2"}, + ids: []string{"123e4567-e89b-12d3-a456-426614174000", "controller-ws-2", "ws-name"}, sort: "id.asc", }, mockEndpoints: getWorkspaceGetEndpoints( "or=(id.in.(123e4567-e89b-12d3-a456-426614174000),"+ - "controller_id.in.(controller-ws-2))&order=id.asc", + "controller_id.in.(controller-ws-2,ws-name),"+ + "name.in.(123e4567-e89b-12d3-a456-426614174000,controller-ws-2,ws-name))&order=id.asc", http.StatusOK, workspacesGetMixedIDsResponseJSON, ), @@ -725,12 +799,12 @@ func TestGetWorkspaces(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getWorkspaceGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusOK, `[`, ), expected: getExpected[types.Workspaces]{ - err: "GET ${URL}/postgrest/workspaces?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/workspaces?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* unexpected EOF", }, @@ -741,12 +815,12 @@ func TestGetWorkspaces(t *testing.T) { ids: []string{"id-a"}, }, mockEndpoints: getWorkspaceGetEndpoints( - "controller_id=in.(id-a)", + "or=(controller_id.in.(id-a),name.in.(id-a))", http.StatusUnauthorized, `unauthorized`, ), expected: getExpected[types.Workspaces]{ - err: "GET ${URL}/postgrest/workspaces?controller_id=in.%28id-a%29 failed\n" + + err: "GET ${URL}/postgrest/workspaces?or=%28controller_id.in.%28id-a%29%2Cname.in.%28id-a%29%29 failed\n" + " Caused by:\n" + "\t* 401 unauthorized", }, @@ -892,12 +966,14 @@ func TestDescribeWorkspaces(t *testing.T) { } func getWorkspaceUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getWorkspaceGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkspaceUpdate), @@ -908,17 +984,18 @@ func getWorkspaceUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getWorkspaceBulkUpdateEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getWorkspaceGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkspaceBulkUpdate), @@ -929,17 +1006,18 @@ func getWorkspaceBulkUpdateEndpoints( Status: responseStatus, Body: responseBody, }, - }, - } + }) } func getWorkspaceDeleteEndpoints( + expectedQuery string, + resolveResponseBody string, expectedBody string, responseStatus int, responseBody string, ) []stubs.StubHTTPEndpoint { - return []stubs.StubHTTPEndpoint{ - { + return append(getWorkspaceGetEndpoints(expectedQuery, http.StatusOK, resolveResponseBody), + stubs.StubHTTPEndpoint{ Request: stubs.StubHTTPRequest{ Method: http.MethodPost, URL: postgrest.Endpoint(constants.WorkbenchEndpointRPCWorkspaceBulkDelete), @@ -950,8 +1028,7 @@ func getWorkspaceDeleteEndpoints( Body: responseBody, Status: responseStatus, }, - }, - } + }) } func getWorkspaceCreateEndpoints( diff --git a/internal/stubs/common_endpoints.go b/internal/stubs/common_endpoints.go index 2e0fa89..372cf5e 100644 --- a/internal/stubs/common_endpoints.go +++ b/internal/stubs/common_endpoints.go @@ -12,8 +12,8 @@ import ( ) const ( - expectedWorkspaceQueryHome = "controller_id=in.(ws-home)" - expectedWorkspaceQueryData = "controller_id=in.(ws-data)" + expectedWorkspaceQueryHome = "or=(controller_id.in.(ws-home),name.in.(ws-home))" + expectedWorkspaceQueryData = "or=(controller_id.in.(ws-data),name.in.(ws-data))" expectedWorkspaceResponseBodyHome = `[{"id":"ws-home","controller_id":"ctrl-home","name":"home",` + `"properties":{"HOME_DIRECTORY":"true"}}]` expectedWorkspaceResponseBodyData = `[{"id":"ws-data","controller_id":"ctrl-data","name":"data",` + diff --git a/internal/types/types.go b/internal/types/types.go index f670999..b05a6b9 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -13,6 +13,14 @@ import ( corev1 "k8s.io/api/core/v1" ) +// IdentifiableResource is implemented by resources that can be identified by +// an ID, (optionally) a controller ID, and a name. +type IdentifiableResource interface { + GetID() string + GetControllerID() string + GetName() string +} + // Config holds CLI configuration values. type Config struct { Endpoint string `json:"endpoint,omitempty"` @@ -93,6 +101,24 @@ type Job struct { Mounts []MountRequest `json:"mounts,omitempty" wbctl:"alias:volumes,editable"` } +// GetID returns the ID of the job. +func (r Job) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the job. Jobs do not have a +// controller ID, so this always returns an empty string. +func (r Job) GetControllerID() string { + return "" +} + +// GetName returns the name of the job. +func (r Job) GetName() string { + return r.Name +} + +var _ IdentifiableResource = (*Job)(nil) + // Jobs is a slice of Job values. type Jobs []Job @@ -131,6 +157,23 @@ type JobSubmission struct { LogURL string `json:"log_url,omitempty"` } +// GetID returns the ID of the job submission. +func (r JobSubmission) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the job submission. +func (r JobSubmission) GetControllerID() string { + return r.ControllerID +} + +// GetName returns the job name of the job submission. +func (r JobSubmission) GetName() string { + return r.JobName +} + +var _ IdentifiableResource = (*JobSubmission)(nil) + // JobSubmissions is a slice of JobSubmission values. type JobSubmissions []JobSubmission @@ -186,6 +229,23 @@ type Workspace struct { Username string `json:"username,omitempty"` } +// GetID returns the ID of the workspace. +func (r Workspace) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the workspace. +func (r Workspace) GetControllerID() string { + return r.ControllerID +} + +// GetName returns the name of the workspace. +func (r Workspace) GetName() string { + return r.Name +} + +var _ IdentifiableResource = (*Workspace)(nil) + // Workspaces is a slice of Workspace values. type Workspaces []Workspace @@ -223,6 +283,23 @@ type Workbench struct { Mounts []MountRequest `json:"mounts,omitempty" wbctl:"alias:volumes,editable"` } +// GetID returns the ID of the workbench. +func (r Workbench) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the workbench. +func (r Workbench) GetControllerID() string { + return r.ControllerID +} + +// GetName returns the name of the workbench. +func (r Workbench) GetName() string { + return r.Name +} + +var _ IdentifiableResource = (*Workbench)(nil) + // Workbenches is a slice of Workbench values. type Workbenches []Workbench @@ -312,6 +389,23 @@ type ImageBuild struct { PodStatus []ExecuteableResourceStatus `json:"pod_status,omitempty"` } +// GetID returns the ID of the image build. +func (r ImageBuild) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the image build. +func (r ImageBuild) GetControllerID() string { + return r.ControllerID +} + +// GetName returns the name of the image build. +func (r ImageBuild) GetName() string { + return r.Name +} + +var _ IdentifiableResource = (*ImageBuild)(nil) + // ImageBuilds is a slice of ImageBuild values. type ImageBuilds []ImageBuild @@ -348,6 +442,23 @@ type Image struct { Username string `json:"username,omitempty"` } +// GetID returns the ID of the image. +func (r Image) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the image. +func (r Image) GetControllerID() string { + return r.ControllerID +} + +// GetName returns the name of the image. +func (r Image) GetName() string { + return r.Name +} + +var _ IdentifiableResource = (*Image)(nil) + // Images is a slice of Image values. type Images []Image @@ -417,6 +528,23 @@ type Snapshot struct { WorkspaceSource *Workspace `json:"workspace_source,omitempty" wbctl:"alias:volume_source"` } +// GetID returns the ID of the snapshot. +func (r Snapshot) GetID() string { + return r.ID +} + +// GetControllerID returns the controller ID of the snapshot. +func (r Snapshot) GetControllerID() string { + return r.ControllerID +} + +// GetName returns the name of the snapshot. +func (r Snapshot) GetName() string { + return r.Name +} + +var _ IdentifiableResource = (*Snapshot)(nil) + // VolumeStatus describes a snapshot volume status. type VolumeStatus struct { Name string `json:"name,omitempty"` diff --git a/internal/types/types_test.go b/internal/types/types_test.go index 7bd7cac..72d3fbf 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -470,6 +470,108 @@ func TestAsWorkspaceSlice(t *testing.T) { } } +func TestIdentifiableResourceGetters(t *testing.T) { + cases := []struct { + name string + resource types.IdentifiableResource + expectedID string + expectedControllerID string + expectedName string + }{ + { + name: "zero value job returns empty strings", + resource: types.Job{}, + expectedID: "", + expectedControllerID: "", + expectedName: "", + }, + { + name: "image build returns ID, controller ID, and name", + resource: types.ImageBuild{ + ID: "image-build-1", + ControllerID: "controller-1", + Name: "image-build-name", + }, + expectedID: "image-build-1", + expectedControllerID: "controller-1", + expectedName: "image-build-name", + }, + { + name: "image returns ID, controller ID, and name", + resource: types.Image{ + ID: "image-1", + ControllerID: "controller-1", + Name: "image-name", + }, + expectedID: "image-1", + expectedControllerID: "controller-1", + expectedName: "image-name", + }, + { + name: "job submission returns job name as name", + resource: types.JobSubmission{ + ID: "job-submission-1", + ControllerID: "controller-1", + JobName: "job-name", + }, + expectedID: "job-submission-1", + expectedControllerID: "controller-1", + expectedName: "job-name", + }, + { + name: "job returns ID and name with empty controller ID", + resource: types.Job{ + ID: "job-1", + Name: "job-name", + }, + expectedID: "job-1", + expectedControllerID: "", + expectedName: "job-name", + }, + { + name: "snapshot returns ID, controller ID, and name", + resource: types.Snapshot{ + ID: "snapshot-1", + ControllerID: "controller-1", + Name: "snapshot-name", + }, + expectedID: "snapshot-1", + expectedControllerID: "controller-1", + expectedName: "snapshot-name", + }, + { + name: "workspace returns ID, controller ID, and name", + resource: types.Workspace{ + ID: "workspace-1", + ControllerID: "controller-1", + Name: "workspace-name", + }, + expectedID: "workspace-1", + expectedControllerID: "controller-1", + expectedName: "workspace-name", + }, + { + name: "workbench returns ID, controller ID, and name", + resource: types.Workbench{ + ID: "workbench-1", + ControllerID: "controller-1", + Name: "workbench-name", + }, + expectedID: "workbench-1", + expectedControllerID: "controller-1", + expectedName: "workbench-name", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expectedID, tc.resource.GetID()) + require.Equal(t, tc.expectedControllerID, tc.resource.GetControllerID()) + require.Equal(t, tc.expectedName, tc.resource.GetName()) + }) + } +} + func TestGetFunction(t *testing.T) { metadata := []types.FunctionMetadata[string]{ {