diff --git a/.changelog/1508.txt b/.changelog/1508.txt new file mode 100644 index 0000000000..c5be0a4489 --- /dev/null +++ b/.changelog/1508.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +workers_for_platforms: Add ability to list Workers for Platforms namespaces, get a namespace, create a new namespace or delete a namespace. +``` + +```release-note:enhancement +workers: Add Workers for Platforms support for getting a Worker, content and bindings +``` diff --git a/workers.go b/workers.go index a98e005c3c..c904d9421c 100644 --- a/workers.go +++ b/workers.go @@ -262,6 +262,14 @@ func (api *API) DeleteWorker(ctx context.Context, rc *ResourceContainer, params // // API reference: https://developers.cloudflare.com/api/operations/worker-script-download-worker func (api *API) GetWorker(ctx context.Context, rc *ResourceContainer, scriptName string) (WorkerScriptResponse, error) { + return api.GetWorkerWithDispatchNamespace(ctx, rc, scriptName, "") +} + +// GetWorker fetch raw script content for your worker returns string containing +// worker code js. +// +// API reference: https://developers.cloudflare.com/api/operations/worker-script-download-worker +func (api *API) GetWorkerWithDispatchNamespace(ctx context.Context, rc *ResourceContainer, scriptName string, dispatchNamespace string) (WorkerScriptResponse, error) { if rc.Level != AccountRouteLevel { return WorkerScriptResponse{}, ErrRequiredAccountLevelResourceContainer } @@ -271,6 +279,9 @@ func (api *API) GetWorker(ctx context.Context, rc *ResourceContainer, scriptName } uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, scriptName) + if dispatchNamespace != "" { + uri = fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s/scripts/%s/content", rc.Identifier, dispatchNamespace, scriptName) + } res, err := api.makeRequestContextWithHeadersComplete(ctx, http.MethodGet, uri, nil, nil) var r WorkerScriptResponse if err != nil { @@ -354,7 +365,7 @@ func (api *API) UploadWorker(ctx context.Context, rc *ResourceContainer, params } uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, params.ScriptName) - if params.DispatchNamespaceName != nil { + if params.DispatchNamespaceName != nil && *params.DispatchNamespaceName != "" { uri = fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s/scripts/%s", rc.Identifier, *params.DispatchNamespaceName, params.ScriptName) } diff --git a/workers_bindings.go b/workers_bindings.go index c362df576f..0516c5b3f7 100644 --- a/workers_bindings.go +++ b/workers_bindings.go @@ -49,7 +49,8 @@ const ( ) type ListWorkerBindingsParams struct { - ScriptName string + ScriptName string + DispatchNamespace *string } // WorkerBindingListItem a struct representing an individual binding in a list of bindings. @@ -426,9 +427,9 @@ func (b WorkerD1DatabaseBinding) serialize(bindingName string) (workerBindingMet } return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "id": b.DatabaseID, + "name": bindingName, + "type": b.Type(), + "id": b.DatabaseID, }, nil, nil } @@ -468,6 +469,9 @@ func (api *API) ListWorkerBindings(ctx context.Context, rc *ResourceContainer, p } uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings", rc.Identifier, params.ScriptName) + if params.DispatchNamespace != nil && *params.DispatchNamespace != "" { + uri = fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s/scripts/%s/bindings", rc.Identifier, *params.DispatchNamespace, params.ScriptName) + } var jsonRes struct { Response diff --git a/workers_bindings_test.go b/workers_bindings_test.go index 677acb9348..bb7f141fa8 100644 --- a/workers_bindings_test.go +++ b/workers_bindings_test.go @@ -108,6 +108,91 @@ func TestListWorkerBindings(t *testing.T) { assert.Equal(t, WorkerD1DataseBindingType, res.BindingList[8].Binding.Type()) } +func TestListWorkerBindings_Wfp(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/dispatch/namespaces/my-namespace/scripts/my-script/bindings", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, listBindingsResponseData) + }) + + res, err := client.ListWorkerBindings(context.Background(), AccountIdentifier(testAccountID), ListWorkerBindingsParams{ + ScriptName: "my-script", + DispatchNamespace: &[]string{"my-namespace"}[0], + }) + assert.NoError(t, err) + + assert.Equal(t, successResponse, res.Response) + assert.Equal(t, 9, len(res.BindingList)) + + assert.Equal(t, res.BindingList[0], WorkerBindingListItem{ + Name: "MY_KV", + Binding: WorkerKvNamespaceBinding{ + NamespaceID: "89f5f8fd93f94cb98473f6f421aa3b65", + }, + }) + assert.Equal(t, WorkerKvNamespaceBindingType, res.BindingList[0].Binding.Type()) + + // WASM binding - No binding content endpoint exists for WfP + + assert.Equal(t, res.BindingList[2], WorkerBindingListItem{ + Name: "MY_PLAIN_TEXT", + Binding: WorkerPlainTextBinding{ + Text: "text", + }, + }) + assert.Equal(t, WorkerPlainTextBindingType, res.BindingList[2].Binding.Type()) + + assert.Equal(t, res.BindingList[3], WorkerBindingListItem{ + Name: "MY_SECRET_TEXT", + Binding: WorkerSecretTextBinding{}, + }) + assert.Equal(t, WorkerSecretTextBindingType, res.BindingList[3].Binding.Type()) + + environment := "MY_ENVIRONMENT" + assert.Equal(t, res.BindingList[4], WorkerBindingListItem{ + Name: "MY_SERVICE_BINDING", + Binding: WorkerServiceBinding{ + Service: "MY_SERVICE", + Environment: &environment, + }, + }) + assert.Equal(t, WorkerServiceBindingType, res.BindingList[4].Binding.Type()) + + assert.Equal(t, res.BindingList[5], WorkerBindingListItem{ + Name: "MY_NEW_BINDING", + Binding: WorkerInheritBinding{}, + }) + assert.Equal(t, WorkerInheritBindingType, res.BindingList[5].Binding.Type()) + + assert.Equal(t, res.BindingList[6], WorkerBindingListItem{ + Name: "MY_BUCKET", + Binding: WorkerR2BucketBinding{ + BucketName: "bucket", + }, + }) + assert.Equal(t, WorkerR2BucketBindingType, res.BindingList[6].Binding.Type()) + + assert.Equal(t, res.BindingList[7], WorkerBindingListItem{ + Name: "MY_DATASET", + Binding: WorkerAnalyticsEngineBinding{ + Dataset: "my_dataset", + }, + }) + + assert.Equal(t, WorkerAnalyticsEngineBindingType, res.BindingList[7].Binding.Type()) + + assert.Equal(t, res.BindingList[8], WorkerBindingListItem{ + Name: "MY_DATABASE", + Binding: WorkerD1DatabaseBinding{ + DatabaseID: "cef5331f-e5c7-4c8a-a415-7908ae45f92a", + }, + }) + assert.Equal(t, WorkerD1DataseBindingType, res.BindingList[8].Binding.Type()) +} + func ExampleUnsafeBinding() { pretty := func(meta workerBindingMeta) string { buf := bytes.NewBufferString("") diff --git a/workers_for_platforms.go b/workers_for_platforms.go new file mode 100644 index 0000000000..9fa8492cbb --- /dev/null +++ b/workers_for_platforms.go @@ -0,0 +1,139 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/goccy/go-json" +) + +type WorkersForPlatformsDispatchNamespace struct { + NamespaceId string `json:"namespace_id"` + NamespaceName string `json:"namespace_name"` + CreatedOn time.Time `json:"created_on"` + CreatedBy string `json:"created_by"` + ModifiedOn time.Time `json:"modified_on"` + ModifiedBy string `json:"modified_by"` +} + +type ListWorkersForPlatformsDispatchNamespaceResponse struct { + Response + Result []WorkersForPlatformsDispatchNamespace `json:"result"` +} + +type GetWorkersForPlatformsDispatchNamespaceResponse struct { + Response + Result WorkersForPlatformsDispatchNamespace `json:"result"` +} + +type CreateWorkersForPlatformsDispatchNamespaceParams struct { + Name string `json:"name"` +} + +// ListWorkersForPlatformsDispatchNamespaces lists the dispatch namespaces. +// +// API reference: https://developers.cloudflare.com/api/operations/namespace-worker-list +func (api *API) ListWorkersForPlatformsDispatchNamespaces(ctx context.Context, rc *ResourceContainer) (*ListWorkersForPlatformsDispatchNamespaceResponse, error) { + if rc.Level != AccountRouteLevel { + return nil, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return nil, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + + var r ListWorkersForPlatformsDispatchNamespaceResponse + if err != nil { + return nil, err + } + + err = json.Unmarshal(res, &r) + if err != nil { + return nil, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return &r, nil +} + +// GetWorkersForPlatformsDispatchNamespace gets a specific dispatch namespace. +// +// API reference: https://developers.cloudflare.com/api/operations/namespace-worker-get-namespace +func (api *API) GetWorkersForPlatformsDispatchNamespace(ctx context.Context, rc *ResourceContainer, name string) (*GetWorkersForPlatformsDispatchNamespaceResponse, error) { + if rc.Level != AccountRouteLevel { + return nil, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return nil, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s", rc.Identifier, name) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + + var r GetWorkersForPlatformsDispatchNamespaceResponse + if err != nil { + return nil, err + } + + err = json.Unmarshal(res, &r) + if err != nil { + return nil, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return &r, nil +} + +// CreateWorkersForPlatformsDispatchNamespace creates a new dispatch namespace. +// +// API reference: https://developers.cloudflare.com/api/operations/namespace-worker-create +func (api *API) CreateWorkersForPlatformsDispatchNamespace(ctx context.Context, rc *ResourceContainer, params CreateWorkersForPlatformsDispatchNamespaceParams) (*GetWorkersForPlatformsDispatchNamespaceResponse, error) { + if rc.Level != AccountRouteLevel { + return nil, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return nil, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + + var r GetWorkersForPlatformsDispatchNamespaceResponse + if err != nil { + return nil, err + } + + err = json.Unmarshal(res, &r) + if err != nil { + return nil, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return &r, nil +} + +// DeleteWorkersForPlatformsDispatchNamespace deletes a dispatch namespace. +// +// API reference: https://developers.cloudflare.com/api/operations/namespace-worker-delete-namespace +func (api *API) DeleteWorkersForPlatformsDispatchNamespace(ctx context.Context, rc *ResourceContainer, name string) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s", rc.Identifier, name) + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + + if err != nil { + return err + } + + return nil +} diff --git a/workers_for_platforms_test.go b/workers_for_platforms_test.go new file mode 100644 index 0000000000..720fcc0b95 --- /dev/null +++ b/workers_for_platforms_test.go @@ -0,0 +1,140 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + listDispatchNamespaces = `{ + "result": [ + { + "namespace_id": "6446f71d-13b3-4bbc-a8a4-9e18760499c8", + "namespace_name": "test", + "created_on": "2024-02-20T17:26:15.4134Z", + "created_by": "4e599df4216133509abaac54b109a647", + "modified_on": "2024-02-20T17:26:15.4134Z", + "modified_by": "4e599df4216133509abaac54b109a647" + }, + { + "namespace_id": "d6851dad-d412-4509-ae13-a364bc5f125a", + "namespace_name": "test-2", + "created_on": "2024-02-20T20:28:36.560575Z", + "created_by": "4e599df4216133509abaac54b109a647", + "modified_on": "2024-02-20T20:28:36.560575Z", + "modified_by": "4e599df4216133509abaac54b109a647" + } + ], + "success": true, + "errors": [], + "messages": [] +}` + + getDispatchNamespace = `{ + "result": { + "namespace_id": "6446f71d-13b3-4bbc-a8a4-9e18760499c8", + "namespace_name": "test", + "created_on": "2024-02-20T17:26:15.4134Z", + "created_by": "4e599df4216133509abaac54b109a647", + "modified_on": "2024-02-20T17:26:15.4134Z", + "modified_by": "4e599df4216133509abaac54b109a647" + }, + "success": true, + "errors": [], + "messages": [] +}` + + deleteDispatchNamespace = `{ + "result": null, + "success": true, + "errors": [], + "messages": [] +}` +) + +func TestListWorkersForPlatformsDispatchNamespaces(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/dispatch/namespaces", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, listDispatchNamespaces) + }) + + res, err := client.ListWorkersForPlatformsDispatchNamespaces(context.Background(), AccountIdentifier(testAccountID)) + + assert.NoError(t, err) + assert.Len(t, res.Result, 2) + + assert.Equal(t, "6446f71d-13b3-4bbc-a8a4-9e18760499c8", res.Result[0].NamespaceId) + assert.Equal(t, "test", res.Result[0].NamespaceName) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result[0].CreatedBy) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result[0].ModifiedBy) + + assert.Equal(t, "d6851dad-d412-4509-ae13-a364bc5f125a", res.Result[1].NamespaceId) + assert.Equal(t, "test-2", res.Result[1].NamespaceName) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result[1].CreatedBy) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result[1].ModifiedBy) +} + +func TestGetWorkersForPlatformsDispatchNamespace(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/dispatch/namespaces/test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, getDispatchNamespace) + }) + + res, err := client.GetWorkersForPlatformsDispatchNamespace(context.Background(), AccountIdentifier(testAccountID), "test") + + assert.NoError(t, err) + + assert.Equal(t, "6446f71d-13b3-4bbc-a8a4-9e18760499c8", res.Result.NamespaceId) + assert.Equal(t, "test", res.Result.NamespaceName) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result.CreatedBy) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result.ModifiedBy) +} + +func TestCreateWorkersForPlatformsDispatchNamespace(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/dispatch/namespaces", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, getDispatchNamespace) + }) + + res, err := client.CreateWorkersForPlatformsDispatchNamespace(context.Background(), AccountIdentifier(testAccountID), CreateWorkersForPlatformsDispatchNamespaceParams{ + Name: "test", + }) + + assert.NoError(t, err) + + assert.Equal(t, "6446f71d-13b3-4bbc-a8a4-9e18760499c8", res.Result.NamespaceId) + assert.Equal(t, "test", res.Result.NamespaceName) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result.CreatedBy) + assert.Equal(t, "4e599df4216133509abaac54b109a647", res.Result.ModifiedBy) +} + +func TestDeleteWorkersForPlatformsDispatchNamespace(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/dispatch/namespaces/test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, deleteDispatchNamespace) + }) + + err := client.DeleteWorkersForPlatformsDispatchNamespace(context.Background(), AccountIdentifier(testAccountID), "test") + + assert.NoError(t, err) +} diff --git a/workers_test.go b/workers_test.go index 2beb16ff38..d83e2a0098 100644 --- a/workers_test.go +++ b/workers_test.go @@ -504,6 +504,30 @@ func TestGetWorker_Module(t *testing.T) { } } +func TestGetWorkerWithDispatchNamespace_Module(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/dispatch/namespaces/bar/scripts/foo/content", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "multipart/form-data; boundary=workermodulescriptdownload") + fmt.Fprint(w, workerModuleScriptDownloadResponse) + }) + + res, err := client.GetWorkerWithDispatchNamespace(context.Background(), AccountIdentifier(testAccountID), "foo", "bar") + want := WorkerScriptResponse{ + successResponse, + true, + WorkerScript{ + Script: workerModuleScript, + }, + } + + if assert.NoError(t, err) { + assert.Equal(t, want.Script, res.Script) + } +} + func TestGetWorkersScriptContent(t *testing.T) { setup() defer teardown()