Repository: cloudflare/cloudflare-go (go)

Issue:
Support Worker Cron Trigger
Hello,

Seems like Worker Cron Triggers were made available recently, it would be neat to control them using terraform

I might be able to spend some time on this next weekend

https://blog.cloudflare.com/introducing-cron-triggers-for-cloudflare-workers/
https://blog.cloudflare.com/cron-triggers-for-scheduled-workers/
https://developers.cloudflare.com/workers/platform/cron-triggers
https://api.cloudflare.com/#worker-cron-trigger-properties


Pull request description:
worker_cron_triggers: Add initial support
Introduces support for Worker Cron Triggers. As of right now, there is
only two available endpoints; one for listing all schedules and one for
updating.

- https://api.cloudflare.com/#worker-cron-trigger-get-cron-triggers
- https://api.cloudflare.com/#worker-cron-trigger-update-cron-triggers

Closes #549


Patch:
diff --git a/LICENSE b/LICENSE
index d9b277f83..20451115d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015-2020, Cloudflare. All rights reserved.
+Copyright (c) 2015-2021, Cloudflare. All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
 are permitted provided that the following conditions are met:
diff --git a/argo_tunnel.go b/argo_tunnel.go
index 0318ef670..dc31f0319 100644
--- a/argo_tunnel.go
+++ b/argo_tunnel.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"net/http"
 	"time"
 
 	"github.com/pkg/errors"
@@ -46,7 +47,7 @@ type ArgoTunnelDetailResponse struct {
 func (api *API) ArgoTunnels(ctx context.Context, accountID string) ([]ArgoTunnel, error) {
 	uri := "/accounts/" + accountID + "/tunnels"
 
-	res, err := api.makeRequestContext(ctx, "GET", uri, nil)
+	res, err := api.makeRequestContextWithHeaders(ctx, "GET", uri, nil, argoV1Header())
 	if err != nil {
 		return []ArgoTunnel{}, errors.Wrap(err, errMakeRequestError)
 	}
@@ -65,7 +66,7 @@ func (api *API) ArgoTunnels(ctx context.Context, accountID string) ([]ArgoTunnel
 func (api *API) ArgoTunnel(ctx context.Context, accountID, tunnelUUID string) (ArgoTunnel, error) {
 	uri := fmt.Sprintf("/accounts/%s/tunnels/%s", accountID, tunnelUUID)
 
-	res, err := api.makeRequestContext(ctx, "GET", uri, nil)
+	res, err := api.makeRequestContextWithHeaders(ctx, "GET", uri, nil, argoV1Header())
 	if err != nil {
 		return ArgoTunnel{}, errors.Wrap(err, errMakeRequestError)
 	}
@@ -86,7 +87,7 @@ func (api *API) CreateArgoTunnel(ctx context.Context, accountID, name, secret st
 
 	tunnel := ArgoTunnel{Name: name, Secret: secret}
 
-	res, err := api.makeRequestContext(ctx, "POST", uri, tunnel)
+	res, err := api.makeRequestContextWithHeaders(ctx, "POST", uri, tunnel, argoV1Header())
 	if err != nil {
 		return ArgoTunnel{}, errors.Wrap(err, errMakeRequestError)
 	}
@@ -96,6 +97,7 @@ func (api *API) CreateArgoTunnel(ctx context.Context, accountID, name, secret st
 	if err != nil {
 		return ArgoTunnel{}, errors.Wrap(err, errUnmarshalError)
 	}
+
 	return argoDetailsResponse.Result, nil
 }
 
@@ -105,7 +107,7 @@ func (api *API) CreateArgoTunnel(ctx context.Context, accountID, name, secret st
 func (api *API) DeleteArgoTunnel(ctx context.Context, accountID, tunnelUUID string) error {
 	uri := fmt.Sprintf("/accounts/%s/tunnels/%s", accountID, tunnelUUID)
 
-	res, err := api.makeRequestContext(ctx, "DELETE", uri, nil)
+	res, err := api.makeRequestContextWithHeaders(ctx, "DELETE", uri, nil, argoV1Header())
 	if err != nil {
 		return errors.Wrap(err, errMakeRequestError)
 	}
@@ -125,7 +127,7 @@ func (api *API) DeleteArgoTunnel(ctx context.Context, accountID, tunnelUUID stri
 func (api *API) CleanupArgoTunnelConnections(ctx context.Context, accountID, tunnelUUID string) error {
 	uri := fmt.Sprintf("/accounts/%s/tunnels/%s/connections", accountID, tunnelUUID)
 
-	res, err := api.makeRequestContext(ctx, "DELETE", uri, nil)
+	res, err := api.makeRequestContextWithHeaders(ctx, "DELETE", uri, nil, argoV1Header())
 	if err != nil {
 		return errors.Wrap(err, errMakeRequestError)
 	}
@@ -138,3 +140,14 @@ func (api *API) CleanupArgoTunnelConnections(ctx context.Context, accountID, tun
 
 	return nil
 }
+
+// The early implementation of Argo Tunnel endpoints didn't conform to the V4
+// API standard response structure. This has been remedied going forward however
+// to support older clients this isn't yet the default. An explicit `Accept`
+// header is used to get the V4 compatible version.
+func argoV1Header() http.Header {
+	header := make(http.Header)
+	header.Set("Accept", "application/json;version=1")
+
+	return header
+}
diff --git a/cloudflare.go b/cloudflare.go
index 77a7df085..1cdd6a9a0 100644
--- a/cloudflare.go
+++ b/cloudflare.go
@@ -172,6 +172,10 @@ func (api *API) makeRequestContext(ctx context.Context, method, uri string, para
 	return api.makeRequestWithAuthType(ctx, method, uri, params, api.authType)
 }
 
+func (api *API) makeRequestContextWithHeaders(ctx context.Context, method, uri string, params interface{}, headers http.Header) ([]byte, error) {
+	return api.makeRequestWithAuthTypeAndHeaders(ctx, method, uri, params, api.authType, headers)
+}
+
 func (api *API) makeRequestWithHeaders(method, uri string, params interface{}, headers http.Header) ([]byte, error) {
 	return api.makeRequestWithAuthTypeAndHeaders(context.TODO(), method, uri, params, api.authType, headers)
 }
diff --git a/workers_cron_triggers.go b/workers_cron_triggers.go
new file mode 100644
index 000000000..222ebc319
--- /dev/null
+++ b/workers_cron_triggers.go
@@ -0,0 +1,75 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// WorkerCronTriggerResponse represents the response from the Worker cron trigger
+// API endpoint.
+type WorkerCronTriggerResponse struct {
+	Response
+	Result WorkerCronTriggerSchedules `json:"result"`
+}
+
+// WorkerCronTriggerSchedules contains the schedule of Worker cron triggers.
+type WorkerCronTriggerSchedules struct {
+	Schedules []WorkerCronTrigger `json:"schedules"`
+}
+
+// WorkerCronTrigger holds an individual cron schedule for a worker.
+type WorkerCronTrigger struct {
+	Cron       string     `json:"cron"`
+	CreatedOn  *time.Time `json:"created_on,omitempty"`
+	ModifiedOn *time.Time `json:"modified_on,omitempty"`
+}
+
+// ListWorkerCronTriggers fetches all available cron triggers for a single Worker
+// script.
+//
+// API reference: https://api.cloudflare.com/#worker-cron-trigger-get-cron-triggers
+func (api *API) ListWorkerCronTriggers(ctx context.Context, scriptName string) ([]WorkerCronTrigger, error) {
+	if err := api.checkAccountID(); err != nil {
+		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", api.AccountID, scriptName)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := WorkerCronTriggerResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return []WorkerCronTrigger{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result.Result.Schedules, err
+}
+
+// UpdateWorkerCronTriggers updates a single schedule for a Worker cron trigger.
+//
+// API reference: https://api.cloudflare.com/#worker-cron-trigger-update-cron-triggers
+func (api *API) UpdateWorkerCronTriggers(ctx context.Context, scriptName string, crons []WorkerCronTrigger) ([]WorkerCronTrigger, error) {
+	if err := api.checkAccountID(); err != nil {
+		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", api.AccountID, scriptName)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, crons)
+	if err != nil {
+		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := WorkerCronTriggerResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return []WorkerCronTrigger{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result.Result.Schedules, err
+}


Test patch:
diff --git a/workers_cron_triggers_test.go b/workers_cron_triggers_test.go
new file mode 100644
index 000000000..8f910b612
--- /dev/null
+++ b/workers_cron_triggers_test.go
@@ -0,0 +1,87 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestListWorkerCronTriggers(t *testing.T) {
+	setup(UsingAccount("9a7806061c88ada191ed06f989cc3dac"))
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"schedules": [
+					{
+						"cron": "*/30 * * * *",
+						"created_on": "2017-01-01T00:00:00Z",
+						"modified_on": "2017-01-01T00:00:00Z"
+					}
+				]
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/9a7806061c88ada191ed06f989cc3dac/workers/scripts/example-script/schedules", handler)
+	createdOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
+	want := []WorkerCronTrigger{{
+		Cron:       "*/30 * * * *",
+		ModifiedOn: &modifiedOn,
+		CreatedOn:  &createdOn,
+	}}
+
+	actual, err := client.ListWorkerCronTriggers(context.Background(), "example-script")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestUpdateWorkerCronTriggers(t *testing.T) {
+	setup(UsingAccount("9a7806061c88ada191ed06f989cc3dac"))
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "PUT", "Expected method 'PUT', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"schedules": [
+					{
+						"cron": "*/30 * * * *",
+						"created_on": "2017-01-01T00:00:00Z",
+						"modified_on": "2017-01-01T00:00:00Z"
+					}
+				]
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/9a7806061c88ada191ed06f989cc3dac/workers/scripts/example-script/schedules", handler)
+	createdOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
+	want := []WorkerCronTrigger{{
+		Cron:       "*/30 * * * *",
+		ModifiedOn: &modifiedOn,
+		CreatedOn:  &createdOn,
+	}}
+
+	actual, err := client.UpdateWorkerCronTriggers(context.Background(), "example-script", want)
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}