Skip to content

Commit

Permalink
Merge pull request #1330 from aaronlisman/alisman/dispatch-namespace-…
Browse files Browse the repository at this point in the history
…features

dispatch namespace features
  • Loading branch information
jacobbednarz committed Jul 25, 2023
2 parents eb5f037 + d1c8036 commit f77c504
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .changelog/1330.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
```release-note:enhancement
workers: Add support for uploading scripts to a Workers for Platforms namespace.
```

```release-note:enhancement
workers: Add support for declaring arbitrary bindings with UnsafeBinding.
```

```release-note:enhancement
workers: Add support for uploading workers with Workers for Platforms namespace bindings.
```

```release-note:enhancement
workers: Add `pipeline_hash` field to Workers script response struct.
```
8 changes: 8 additions & 0 deletions workers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type CreateWorkerParams struct {
ScriptName string
Script string

// DispatchNamespaceName uploads the worker to a WFP dispatch namespace if provided
DispatchNamespaceName *string

// Module changes the Content-Type header to specify the script is an
// ES Module syntax script.
Module bool
Expand Down Expand Up @@ -114,6 +117,7 @@ type WorkerMetaData struct {
LastDeployedFrom *string `json:"last_deployed_from,omitempty"`
DeploymentId *string `json:"deployment_id,omitempty"`
PlacementMode *PlacementMode `json:"placement_mode,omitempty"`
PipelineHash *string `json:"pipeline_hash,omitempty"`
}

// WorkerListResponse wrapper struct for API response to worker script list API call.
Expand Down Expand Up @@ -272,6 +276,10 @@ 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 {
uri = fmt.Sprintf("/accounts/%s/workers/namespaces/%s/scripts/%s", rc.Identifier, *params.DispatchNamespaceName, params.ScriptName)
}

headers := make(http.Header)
headers.Set("Content-Type", contentType)
res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)
Expand Down
81 changes: 81 additions & 0 deletions workers_bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
WorkerAnalyticsEngineBindingType WorkerBindingType = "analytics_engine"
// WorkerQueueBindingType is the type for queue bindings.
WorkerQueueBindingType WorkerBindingType = "queue"
// DispatchNamespaceBindingType is the type for WFP namespace bindings.
DispatchNamespaceBindingType WorkerBindingType = "dispatch_namespace"
)

type ListWorkerBindingsParams struct {
Expand Down Expand Up @@ -339,6 +341,85 @@ func (b WorkerQueueBinding) serialize(bindingName string) (workerBindingMeta, wo
}, nil, nil
}

// DispatchNamespaceBinding is a binding to a Workers for Platforms namespace
//
// https://developers.cloudflare.com/workers/platform/bindings/#dispatch-namespace-bindings-workers-for-platforms
type DispatchNamespaceBinding struct {
Binding string
Namespace string
Outbound *NamespaceOutboundOptions
}

type NamespaceOutboundOptions struct {
Worker WorkerReference
Params []OutboundParamSchema
}

type WorkerReference struct {
Service string
Environment *string
}

type OutboundParamSchema struct {
Name string
}

// Type returns the type of the binding.
func (b DispatchNamespaceBinding) Type() WorkerBindingType {
return DispatchNamespaceBindingType
}

func (b DispatchNamespaceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
if b.Binding == "" {
return nil, nil, fmt.Errorf(`Binding name for binding "%s" cannot be empty`, bindingName)
}
if b.Namespace == "" {
return nil, nil, fmt.Errorf(`Namespace name for binding "%s" cannot be empty`, bindingName)
}

meta := workerBindingMeta{
"type": b.Type(),
"name": b.Binding,
"namespace": b.Namespace,
}

if b.Outbound != nil {
if b.Outbound.Worker.Service == "" {
return nil, nil, fmt.Errorf(`Outbound options for binding "%s" must have a service name`, bindingName)
}

var params []map[string]interface{}
for _, param := range b.Outbound.Params {
params = append(params, map[string]interface{}{
"name": param.Name,
})
}

meta["outbound"] = map[string]interface{}{
"worker": map[string]interface{}{
"service": b.Outbound.Worker.Service,
"environment": b.Outbound.Worker.Environment,
},
"params": params,
}
}

return meta, nil, nil
}

// UnsafeBinding is for experimental or deprecated bindings, and allows specifying any binding type or property.
type UnsafeBinding map[string]interface{}

// Type returns the type of the binding.
func (b UnsafeBinding) Type() WorkerBindingType {
return ""
}

func (b UnsafeBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
b["name"] = bindingName
return b, nil, nil
}

// Each binding that adds a part to the multipart form body will need
// a unique part name so we just generate a random 128bit hex string.
func getRandomPartName() string {
Expand Down
45 changes: 45 additions & 0 deletions workers_bindings_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cloudflare

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -95,3 +97,46 @@ func TestListWorkerBindings(t *testing.T) {
})
assert.Equal(t, WorkerAnalyticsEngineBindingType, res.BindingList[7].Binding.Type())
}

func ExampleUnsafeBinding() {
pretty := func(meta workerBindingMeta) string {
buf := bytes.NewBufferString("")
encoder := json.NewEncoder(buf)
encoder.SetIndent("", " ")
if err := encoder.Encode(meta); err != nil {
fmt.Println("error:", err)
}
return buf.String()
}

binding_a := WorkerServiceBinding{
Service: "foo",
}
meta_a, _, _ := binding_a.serialize("my_binding")
meta_a_json := pretty(meta_a)
fmt.Println(meta_a_json)

binding_b := UnsafeBinding{
"type": "service",
"service": "foo",
}
meta_b, _, _ := binding_b.serialize("my_binding")
meta_b_json := pretty(meta_b)
fmt.Println(meta_b_json)

fmt.Println(meta_a_json == meta_b_json)
// Output:
// {
// "name": "my_binding",
// "service": "foo",
// "type": "service"
// }
//
// {
// "name": "my_binding",
// "service": "foo",
// "type": "service"
// }
//
// true
}
129 changes: 129 additions & 0 deletions workers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,64 @@ func TestUploadWorker_WithQueueBinding(t *testing.T) {
assert.NoError(t, err)
}

func TestUploadWorker_WithDispatchNamespaceBinding(t *testing.T) {
setup()
defer teardown()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)

mpUpload, err := parseMultipartUpload(r)
assert.NoError(t, err)

expectedBindings := map[string]workerBindingMeta{
"b1": {
"name": "b1",
"type": "dispatch_namespace",
"namespace": "n1",
"outbound": map[string]interface{}{
"worker": map[string]interface{}{
"service": "w1",
"environment": "e1",
},
"params": []interface{}{
map[string]interface{}{"name": "param1"},
},
},
},
}
assert.Equal(t, workerScript, mpUpload.Script)
assert.Equal(t, expectedBindings, mpUpload.BindingMeta)

w.Header().Set("content-type", "application/json")
fmt.Fprint(w, workersScriptResponse(t))
}
mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)

environmentName := "e1"
_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
ScriptName: "bar",
Script: workerScript,
Bindings: map[string]WorkerBinding{
"b1": DispatchNamespaceBinding{
Binding: "b1",
Namespace: "n1",
Outbound: &NamespaceOutboundOptions{
Worker: WorkerReference{
Service: "w1",
Environment: &environmentName,
},
Params: []OutboundParamSchema{
{
Name: "param1",
},
},
},
},
}})
assert.NoError(t, err)
}

func TestUploadWorker_WithSmartPlacementEnabled(t *testing.T) {
setup()
defer teardown()
Expand Down Expand Up @@ -1140,3 +1198,74 @@ func TestUploadWorker_WithTailConsumers(t *testing.T) {
assert.Len(t, *worker.TailConsumers, 2)
})
}

func TestUploadWorker_ToDispatchNamespace(t *testing.T) {
setup()
defer teardown()

namespaceName := "n1"

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)

mpUpload, err := parseMultipartUpload(r)
require.NoError(t, err)

assert.Equal(t, workerScript, mpUpload.Script)

w.Header().Set("content-type", "application/json")
fmt.Fprint(w, workersScriptResponse(t))
}
mux.HandleFunc(
fmt.Sprintf("/accounts/"+testAccountID+"/workers/namespaces/%s/scripts/bar", namespaceName),
handler,
)

_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
ScriptName: "bar",
Script: workerScript,
DispatchNamespaceName: &namespaceName,
Bindings: map[string]WorkerBinding{
"b1": WorkerPlainTextBinding{
Text: "hello",
},
},
})
assert.NoError(t, err)
}

func TestUploadWorker_UnsafeBinding(t *testing.T) {
setup()
defer teardown()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)

mpUpload, err := parseMultipartUpload(r)
require.NoError(t, err)

assert.Equal(t, workerScript, mpUpload.Script)

require.Contains(t, mpUpload.BindingMeta, "b1")
assert.Contains(t, mpUpload.BindingMeta["b1"], "name")
assert.Equal(t, "b1", mpUpload.BindingMeta["b1"]["name"])
assert.Contains(t, mpUpload.BindingMeta["b1"], "type")
assert.Equal(t, "dynamic_dispatch", mpUpload.BindingMeta["b1"]["type"])

w.Header().Set("content-type", "application/json")
fmt.Println(workersScriptResponse(t))
fmt.Fprint(w, workersScriptResponse(t))
}
mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)

_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
ScriptName: "bar",
Script: workerScript,
Bindings: map[string]WorkerBinding{
"b1": UnsafeBinding{
"type": "dynamic_dispatch",
},
},
})
assert.NoError(t, err)
}

0 comments on commit f77c504

Please sign in to comment.