Repository: cloudflare/cloudflare-go
Pull request #1508 (2024-02-29 14:48:01)

Add Workers for Platforms APIs
## Description

Adds some new APIs for managing Workers for Platforms namespaces and also expands some existing script APIs to support Workers for Platforms.

## Has your change been tested?

Changes have unit tests added and have been tested in the Terraform change also in the works.

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [ ] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) and relies on stable APIs.

API docs in staging

[1]: https://help.github.com/articles/closing-issues-using-keywords/


github-actions[bot]: Oops! It looks like no changelog entry is attached to this PR. Please include a release note as described in https://github.com/cloudflare/cloudflare-go/blob/master/docs/changelog-process.md.

Example: 

~~~
```release-note:TYPE
Release note
```
~~~

If you do not require a release note to be included, please add the `workflow/skip-changelog-entry` label.

Patch:
diff --git a/.changelog/1508.txt b/.changelog/1508.txt
new file mode 100644
index 000000000..c5be0a448
--- /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 a98e005c3..c904d9421 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 c362df576..0516c5b3f 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_for_platforms.go b/workers_for_platforms.go
new file mode 100644
index 000000000..eda77eaee
--- /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,omitempty"`
+	CreatedBy     string     `json:"created_by"`
+	ModifiedOn    *time.Time `json:"modified_on,omitempty"`
+	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
+}


Test patch:
diff --git a/workers_bindings_test.go b/workers_bindings_test.go
index 677acb934..bb7f141fa 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_test.go b/workers_for_platforms_test.go
new file mode 100644
index 000000000..720fcc0b9
--- /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 2beb16ff3..d83e2a009 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()