diff --git a/.changelog/1136.txt b/.changelog/1136.txt new file mode 100644 index 0000000000..976f76ea7c --- /dev/null +++ b/.changelog/1136.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +pages: Updates bindings and other Functions related propreties. Service bindings, secrets, fail open/close and usage model are all now supported. +``` + +```release-note:breaking-change +pages: Changed the type of EnvVars in PagesProjectDeploymentConfigEnvironment & PagesProjectDeployment in order to properly support secrets. +``` diff --git a/.changelog/1137.txt b/.changelog/1137.txt new file mode 100644 index 0000000000..b479d14d44 --- /dev/null +++ b/.changelog/1137.txt @@ -0,0 +1,39 @@ +```release-note:note +workers: all worker methods have been split into product ownership(-ish) files +``` + +```release-note:note +workers: all worker methods now require an explicit `ResourceContainer` for endpoints instead of relying on the globally defined `api.AccountID` +``` + +```release-note:breaking-change +workers: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +workers: API operations now target account level resources instead of older zone level resources (these are a 1:1 now) +``` + +```release-note:breaking-change +workers_bindings: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +workers_kv: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +workers_tails: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +workers_secrets: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +workers_routes: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +workers_cron_triggers: method signatures have been updated to align with the upcoming client conventions +``` diff --git a/.changelog/1139.txt b/.changelog/1139.txt new file mode 100644 index 0000000000..71f26a2e88 --- /dev/null +++ b/.changelog/1139.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.23.5 to 2.23.6 +``` diff --git a/.changelog/1140.txt b/.changelog/1140.txt new file mode 100644 index 0000000000..f6b31ad5a8 --- /dev/null +++ b/.changelog/1140.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +cache_rules: add ignore option to query string struct +``` \ No newline at end of file diff --git a/.changelog/1142.txt b/.changelog/1142.txt new file mode 100644 index 0000000000..48f9efbcd7 --- /dev/null +++ b/.changelog/1142.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +teams_rules: adds support for Egress Policies +``` diff --git a/.changelog/1146.txt b/.changelog/1146.txt new file mode 100644 index 0000000000..76ce4035d9 --- /dev/null +++ b/.changelog/1146.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps goreleaser/goreleaser-action from 3.2.0 to 4.1.0 +``` diff --git a/.changelog/1148.txt b/.changelog/1148.txt new file mode 100644 index 0000000000..55149599e0 --- /dev/null +++ b/.changelog/1148.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +managed_networks: add CRUD functionality for managednetworks +``` \ No newline at end of file diff --git a/.changelog/1149.txt b/.changelog/1149.txt new file mode 100644 index 0000000000..c6c40af07d --- /dev/null +++ b/.changelog/1149.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +tiered_cache: Add support for Tiered Caching interactions for setting Smart and Generic topologies +``` \ No newline at end of file diff --git a/.changelog/1150.txt b/.changelog/1150.txt new file mode 100644 index 0000000000..ef7d48cd3c --- /dev/null +++ b/.changelog/1150.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +mtls_certificate: add support for managing mTLS certificates and assocations +``` diff --git a/.changelog/1151.txt b/.changelog/1151.txt new file mode 100644 index 0000000000..5482d72f8e --- /dev/null +++ b/.changelog/1151.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +dns: add support for tags and comments +``` + +```release-note:breaking-change +dns: method signatures have been updated to align with the upcoming client conventions +``` + +```release-note:breaking-change +dns: `DNSRecords` has been renamed to `ListDNSRecords` +``` + +```release-note:breaking-change +dns: `DNSRecord` has been renamed to `GetDNSRecord` +``` diff --git a/.changelog/1155.txt b/.changelog/1155.txt new file mode 100644 index 0000000000..d2572b3dfa --- /dev/null +++ b/.changelog/1155.txt @@ -0,0 +1,3 @@ +```release-note:bug +workers: correctly set `body` value for non-ES module uploads +``` diff --git a/.changelog/1156.txt b/.changelog/1156.txt new file mode 100644 index 0000000000..2a19167b7d --- /dev/null +++ b/.changelog/1156.txt @@ -0,0 +1,31 @@ +```release-note:bug +firewall_rules: use empty reponse struct on each page call +``` + +```release-note:bug +filter: use empty reponse struct on each page call +``` + +```release-note:bug +email_routing_destination: use empty reponse struct on each page call +``` + +```release-note:bug +email_routing_rules: use empty reponse struct on each page call +``` + +```release-note:bug +lockdown: use empty reponse struct on each page call +``` + +```release-note:bug +queue: use empty reponse struct on each page call +``` + +```release-note:bug +teams_list: use empty reponse struct on each page call +``` + +```release-note:bug +workers_kv: use empty reponse struct on each page call +``` diff --git a/.changelog/1159.txt b/.changelog/1159.txt new file mode 100644 index 0000000000..5aca5dc5ba --- /dev/null +++ b/.changelog/1159.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access_organization: add user_seat_expiration_inactive_time field +``` diff --git a/.changelog/1160.txt b/.changelog/1160.txt new file mode 100644 index 0000000000..b4893dc46b --- /dev/null +++ b/.changelog/1160.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +workers: Add support for workers logpush enablement on script upload +``` diff --git a/.changelog/1161.txt b/.changelog/1161.txt new file mode 100644 index 0000000000..5578937b36 --- /dev/null +++ b/.changelog/1161.txt @@ -0,0 +1,23 @@ +```release-note:enhancement +origin_ca: add support for using API keys, API tokens or API User service keys for interacting with Origin CA endpoints +``` + +```release-note:breaking-change +origin_ca: renamed to `CreateOriginCertificate` to `CreateOriginCACertificate` +``` + +```release-note:breaking-change +origin_ca: renamed to `OriginCertificates` to `ListOriginCACertificates` +``` + +```release-note:breaking-change +origin_ca: renamed to `OriginCertificate` to `GetOriginCACertificate` +``` + +```release-note:breaking-change +origin_ca: renamed to `RevokeOriginCertificate` to `RevokeOriginCACertificate` +``` + +```release-note:breaking-change +origin_ca: renamed to `OriginCARootCertificate` to `GetOriginCARootCertificate` +``` diff --git a/.changelog/1162.txt b/.changelog/1162.txt new file mode 100644 index 0000000000..871465c057 --- /dev/null +++ b/.changelog/1162.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/hashicorp/go-retryablehttp from 0.7.1 to 0.7.2 +``` diff --git a/.changelog/1164.txt b/.changelog/1164.txt new file mode 100644 index 0000000000..72272d5df3 --- /dev/null +++ b/.changelog/1164.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +cloudflare: automatically redact sensitive values from HTTP interactions +``` diff --git a/.changelog/1167.txt b/.changelog/1167.txt new file mode 100644 index 0000000000..6dacacf5e7 --- /dev/null +++ b/.changelog/1167.txt @@ -0,0 +1,3 @@ +```release-note:bug +dns: don't send "priority" for list operations as it isn't supported and is only used for internal filtering +``` diff --git a/.changelog/1170.txt b/.changelog/1170.txt new file mode 100644 index 0000000000..0a3b278ea8 --- /dev/null +++ b/.changelog/1170.txt @@ -0,0 +1,7 @@ +```release-note:note +dns: remove additional lookup from `Update` operations when `Name` or `Type` was omitted +``` + +```release-note:breaking-change +dns: remove these read-only fields from `UpdateDNSRecordParams`: `CreatedOn`, `ModifiedOn`, `Meta`, `ZoneID`, `ZoneName`, `Proxiable`, and `Locked` +``` diff --git a/.changelog/1171.txt b/.changelog/1171.txt new file mode 100644 index 0000000000..174071f005 --- /dev/null +++ b/.changelog/1171.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dns: update default `per_page` attribute to 100 records +``` diff --git a/.changelog/1172.txt b/.changelog/1172.txt new file mode 100644 index 0000000000..9b4a5bff22 --- /dev/null +++ b/.changelog/1172.txt @@ -0,0 +1,3 @@ +```release-note:bug +managednetworks: Update should be PUT +``` diff --git a/.changelog/1173.txt b/.changelog/1173.txt new file mode 100644 index 0000000000..403a2feff3 --- /dev/null +++ b/.changelog/1173.txt @@ -0,0 +1,11 @@ +```release-note:bug +dns: the field `Tags` in `ListDNSRecordsParams` was not correctly serialized into URL queries +``` + +```release-note:enhancement +dns: the URL parameter `tag-match` for listing DNS records is now supported as the field `TagMatch` in `ListDNSRecordsParams` +``` + +```release-note:breaking-change +dns: the fields `CreatedOn` and `ModifiedOn` are removed from `ListDNSRecordsParams` +``` diff --git a/.changelog/1174.txt b/.changelog/1174.txt new file mode 100644 index 0000000000..686be2ad19 --- /dev/null +++ b/.changelog/1174.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dns: `GetDNSRecord`, `UpdateDNSRecord` and `DeleteDNSRecord` now return the new, dedicated error `ErrMissingDNSRecordID` when an empty DNS record ID is given. +``` diff --git a/.changelog/1176.txt b/.changelog/1176.txt new file mode 100644 index 0000000000..4d3e6d894d --- /dev/null +++ b/.changelog/1176.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +workers: script upload now supports Queues bindings +``` \ No newline at end of file diff --git a/.changelog/1177.txt b/.changelog/1177.txt new file mode 100644 index 0000000000..bb9f8d81e3 --- /dev/null +++ b/.changelog/1177.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +workers: Add support for compatibility_date and compatibility_flags when upoading a worker script +``` diff --git a/.changelog/1178.txt b/.changelog/1178.txt new file mode 100644 index 0000000000..08101c917d --- /dev/null +++ b/.changelog/1178.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access_identity_provider: add scim_config field +``` \ No newline at end of file diff --git a/.changelog/1180.txt b/.changelog/1180.txt new file mode 100644 index 0000000000..8d21a99d74 --- /dev/null +++ b/.changelog/1180.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.23.7 to 2.24.1 +``` diff --git a/.changelog/1181.txt b/.changelog/1181.txt new file mode 100644 index 0000000000..355b67584b --- /dev/null +++ b/.changelog/1181.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access_organization: add ui_read_only_toggle_reason field +``` diff --git a/.changelog/1183.txt b/.changelog/1183.txt new file mode 100644 index 0000000000..01e3947686 --- /dev/null +++ b/.changelog/1183.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +rulesets: add support for `score_per_period` and `score_response_header_name` +``` diff --git a/.changelog/1184.txt b/.changelog/1184.txt new file mode 100644 index 0000000000..7e721c577b --- /dev/null +++ b/.changelog/1184.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps dependabot/fetch-metadata from 1.3.5 to 1.3.6 +``` diff --git a/.changelog/1185.txt b/.changelog/1185.txt new file mode 100644 index 0000000000..5cf8562005 --- /dev/null +++ b/.changelog/1185.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +magic_transit_ipsec_tunnel: makes customer endpoint an optional field for ipsec tunnel creation +``` diff --git a/.changelog/1188.txt b/.changelog/1188.txt new file mode 100644 index 0000000000..b478f060d3 --- /dev/null +++ b/.changelog/1188.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +queues: UpdateQueue has been updated to match the API and now correctly updates a Queue's name +``` \ No newline at end of file diff --git a/.changelog/1191.txt b/.changelog/1191.txt new file mode 100644 index 0000000000..f42bc3663c --- /dev/null +++ b/.changelog/1191.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.24.1 to 2.24.2 +``` diff --git a/.changelog/1192.txt b/.changelog/1192.txt new file mode 100644 index 0000000000..23cd06d801 --- /dev/null +++ b/.changelog/1192.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps goreleaser/goreleaser-action from 4.1.0 to 4.2.0 +``` diff --git a/.changelog/1193.txt b/.changelog/1193.txt new file mode 100644 index 0000000000..461d746f78 --- /dev/null +++ b/.changelog/1193.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dlp_profile: Add new allowed_match_count field to profiles +``` diff --git a/.changelog/1195.txt b/.changelog/1195.txt new file mode 100644 index 0000000000..4642d151ee --- /dev/null +++ b/.changelog/1195.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dns: allow sending empty strings to remove comments +``` diff --git a/.changelog/1196.txt b/.changelog/1196.txt new file mode 100644 index 0000000000..fba5681f28 --- /dev/null +++ b/.changelog/1196.txt @@ -0,0 +1,3 @@ +```release-note:bug +dns: always send `tags` to allow clearing +``` diff --git a/.changelog/1197.txt b/.changelog/1197.txt new file mode 100644 index 0000000000..9535b0baeb --- /dev/null +++ b/.changelog/1197.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +teams_account: add support for `check_disks` +``` \ No newline at end of file diff --git a/.changelog/1199.txt b/.changelog/1199.txt new file mode 100644 index 0000000000..f7cb743b3a --- /dev/null +++ b/.changelog/1199.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.24.2 to 2.24.3 +``` diff --git a/.changelog/1200.txt b/.changelog/1200.txt new file mode 100644 index 0000000000..40d4ec4b78 --- /dev/null +++ b/.changelog/1200.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dlp_profile: Use int rather than uint for allowed_match_count field +``` diff --git a/.changelog/1202.txt b/.changelog/1202.txt new file mode 100644 index 0000000000..58c21d62c2 --- /dev/null +++ b/.changelog/1202.txt @@ -0,0 +1,3 @@ +```release-note:bug +stream: renamed `RequiredSignedURLs` to `RequireSignedURLs` +``` diff --git a/.changelog/1205.txt b/.changelog/1205.txt new file mode 100644 index 0000000000..2573522809 --- /dev/null +++ b/.changelog/1205.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +devices_policy: Add new exclude_office_ips field to policy +``` diff --git a/.changelog/1206.txt b/.changelog/1206.txt new file mode 100644 index 0000000000..fb12a08ebe --- /dev/null +++ b/.changelog/1206.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +tunnels: automatically paginate `ListTunnels` +``` diff --git a/.changelog/1207.txt b/.changelog/1207.txt new file mode 100644 index 0000000000..7dd4bc7046 --- /dev/null +++ b/.changelog/1207.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +cloudflare: make it clearer when we hit a server error and to retry later +``` diff --git a/.changelog/1208.txt b/.changelog/1208.txt new file mode 100644 index 0000000000..8075ecc793 --- /dev/null +++ b/.changelog/1208.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +teams_accounts: Add new root_certificate_installation_enabled field +``` diff --git a/.changelog/1209.txt b/.changelog/1209.txt new file mode 100644 index 0000000000..bc83d55374 --- /dev/null +++ b/.changelog/1209.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dex_test: add CRUD functionality for DEX test configurations +``` \ No newline at end of file diff --git a/.changelog/1210.txt b/.changelog/1210.txt new file mode 100644 index 0000000000..a05f5531d1 --- /dev/null +++ b/.changelog/1210.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.24.3 to 2.24.4 +``` diff --git a/.changelog/1212.txt b/.changelog/1212.txt new file mode 100644 index 0000000000..8656a47b4a --- /dev/null +++ b/.changelog/1212.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dlp: Adds support for partial payload logging +``` diff --git a/.changelog/1213.txt b/.changelog/1213.txt new file mode 100644 index 0000000000..124db5e916 --- /dev/null +++ b/.changelog/1213.txt @@ -0,0 +1,3 @@ +```release-note:bug +dex_test: use dex test types and json struct mappings instead of managed networks +``` diff --git a/.changelog/1214.txt b/.changelog/1214.txt new file mode 100644 index 0000000000..e41d1a146d --- /dev/null +++ b/.changelog/1214.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +teams_rules: Add `untrusted_cert` rule setting +``` \ No newline at end of file diff --git a/.changelog/1215.txt b/.changelog/1215.txt new file mode 100644 index 0000000000..44307cf98b --- /dev/null +++ b/.changelog/1215.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/text from 0.3.7 to 0.3.8 +``` diff --git a/.changelog/1216.txt b/.changelog/1216.txt new file mode 100644 index 0000000000..44307cf98b --- /dev/null +++ b/.changelog/1216.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/text from 0.3.7 to 0.3.8 +``` diff --git a/.changelog/1217.txt b/.changelog/1217.txt new file mode 100644 index 0000000000..0fc959900b --- /dev/null +++ b/.changelog/1217.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/time from 0.0.0-20220224211638-0e9765cccd65 to 0.3.0 +``` diff --git a/.changelog/1218.txt b/.changelog/1218.txt new file mode 100644 index 0000000000..148e60cca6 --- /dev/null +++ b/.changelog/1218.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.0.0-20220722155237-a158d28d115b to 0.7.0 +``` diff --git a/.changelog/1219.txt b/.changelog/1219.txt new file mode 100644 index 0000000000..148e60cca6 --- /dev/null +++ b/.changelog/1219.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.0.0-20220722155237-a158d28d115b to 0.7.0 +``` diff --git a/.changelog/1220.txt b/.changelog/1220.txt new file mode 100644 index 0000000000..6f31f37add --- /dev/null +++ b/.changelog/1220.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/stretchr/testify from 1.8.1 to 1.8.2 +``` diff --git a/.changelog/1222.txt b/.changelog/1222.txt new file mode 100644 index 0000000000..5d9a668a01 --- /dev/null +++ b/.changelog/1222.txt @@ -0,0 +1,3 @@ +```release-note:bug +dns: dont reuse DNSListResponse when using pagination to avoid Proxied pointer overwrite +``` \ No newline at end of file diff --git a/.changelog/1223.txt b/.changelog/1223.txt new file mode 100644 index 0000000000..e046c338bc --- /dev/null +++ b/.changelog/1223.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access_application: Add `path_cookie_attribute` app setting +``` \ No newline at end of file diff --git a/.changelog/1226.txt b/.changelog/1226.txt new file mode 100644 index 0000000000..167b1e6f78 --- /dev/null +++ b/.changelog/1226.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +added audit_ssh to gateway actions, updated gateway rule settings +``` diff --git a/.changelog/1227.txt b/.changelog/1227.txt new file mode 100644 index 0000000000..96c1ce0524 --- /dev/null +++ b/.changelog/1227.txt @@ -0,0 +1,10 @@ +```release-note:breaking-change +tunnel: renamed `Tunnels` to `ListTunnels` +``` + +```release-note:breaking-change +tunnel: renamed `Tunnel` to `GetTunnel` +``` +```release-note:enhancement +tunnel: updated parameters to latest API docs +``` \ No newline at end of file diff --git a/.changelog/1228.txt b/.changelog/1228.txt new file mode 100644 index 0000000000..72816b5f3d --- /dev/null +++ b/.changelog/1228.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.7.0 to 0.8.0 +``` diff --git a/.changelog/1229.txt b/.changelog/1229.txt new file mode 100644 index 0000000000..a4b655ccb5 --- /dev/null +++ b/.changelog/1229.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.24.4 to 2.25.0 +``` diff --git a/.changelog/1232.txt b/.changelog/1232.txt new file mode 100644 index 0000000000..c8e3ce2bdd --- /dev/null +++ b/.changelog/1232.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +addressing: Add `Address Map` support +``` diff --git a/.changelog/1236.txt b/.changelog/1236.txt new file mode 100644 index 0000000000..bf5b7c4ace --- /dev/null +++ b/.changelog/1236.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps actions/setup-go from 3 to 4 +``` diff --git a/.changelog/1237.txt b/.changelog/1237.txt new file mode 100644 index 0000000000..1c87f2721b --- /dev/null +++ b/.changelog/1237.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access_identity_provider: add `claims` and `scopes` fields +``` diff --git a/.changelog/1238.txt b/.changelog/1238.txt new file mode 100644 index 0000000000..e01bf64532 --- /dev/null +++ b/.changelog/1238.txt @@ -0,0 +1,3 @@ +```release-note:bug +tunnel: Fix 'CreateTunnel' for tunnels using config_src +``` \ No newline at end of file diff --git a/.changelog/1242.txt b/.changelog/1242.txt new file mode 100644 index 0000000000..f847da86f8 --- /dev/null +++ b/.changelog/1242.txt @@ -0,0 +1,7 @@ +```release-note:bug +teams_rules: `AllowChildBypass` changes from a `bool` to `*bool` +``` + +```release-note:bug +teams_rules: `BypassParentRule` changes from a `bool` to `*bool` +``` diff --git a/.changelog/1243.txt b/.changelog/1243.txt new file mode 100644 index 0000000000..f5c0728ec9 --- /dev/null +++ b/.changelog/1243.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +dns: Changed Create/UpdateDNSRecord method signatures to return (DNSRecord, error) +``` \ No newline at end of file diff --git a/.changelog/1244.txt b/.changelog/1244.txt new file mode 100644 index 0000000000..fa7858c06f --- /dev/null +++ b/.changelog/1244.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +ssl: make `GeoRestrictions` a pointer inside of ZoneCustomSSL +``` \ No newline at end of file diff --git a/.changelog/1246.txt b/.changelog/1246.txt new file mode 100644 index 0000000000..678b27fd5e --- /dev/null +++ b/.changelog/1246.txt @@ -0,0 +1,3 @@ +```release-note:note +dns_firewall: The `OriginIPs` field has been renamed to `UpstreamIPs`. +``` diff --git a/.changelog/1249.txt b/.changelog/1249.txt new file mode 100644 index 0000000000..0f2cbedf95 --- /dev/null +++ b/.changelog/1249.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +devices_policy: update `Mode` field to use new `ServiceMode` string type with explicit const service mode values +``` diff --git a/.changelog/1250.txt b/.changelog/1250.txt new file mode 100644 index 0000000000..8f1a611d51 --- /dev/null +++ b/.changelog/1250.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.25.0 to 2.25.1 +``` diff --git a/.changelog/1251.txt b/.changelog/1251.txt new file mode 100644 index 0000000000..b750d6ebf6 --- /dev/null +++ b/.changelog/1251.txt @@ -0,0 +1,11 @@ +```release-note:breaking-change +zone: `ZoneSingleSetting` has been renamed to `GetZoneSetting` and updated method signature inline with our expected conventions +``` + +```release-note:breaking-change +zone: `UpdateZoneSingleSetting` has been renamed to `UpdateZoneSetting` and updated method signature inline with our expected conventions +``` + +```release-note:enhancement +zone: `GetZoneSetting` and `UpdateZoneSetting` now allow configuring the path for where a setting resides instead of assuming `settings` +``` diff --git a/.changelog/1253.txt b/.changelog/1253.txt new file mode 100644 index 0000000000..a43f5d8cb7 --- /dev/null +++ b/.changelog/1253.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +rulesets: add support for add operation to HTTP header configuration +``` diff --git a/.changelog/1258.txt b/.changelog/1258.txt new file mode 100644 index 0000000000..09748a6ecf --- /dev/null +++ b/.changelog/1258.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access: Add `isolation_required` flag to Access policies +``` diff --git a/.changelog/1260.txt b/.changelog/1260.txt new file mode 100644 index 0000000000..806345d464 --- /dev/null +++ b/.changelog/1260.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access: Add `auto_redirect_to_identity` flag to Access organizations +``` diff --git a/.changelog/1261.txt b/.changelog/1261.txt new file mode 100644 index 0000000000..efa0a01bad --- /dev/null +++ b/.changelog/1261.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +rulesets: add support for the `http_response_compression` phase +``` + +```release-note:enhancement +rulesets: add support for the `compress_response` action +``` diff --git a/.changelog/1263.txt b/.changelog/1263.txt new file mode 100644 index 0000000000..785f7c0c13 --- /dev/null +++ b/.changelog/1263.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.8.0 to 0.9.0 +``` diff --git a/.changelog/1265.txt b/.changelog/1265.txt new file mode 100644 index 0000000000..f93fc420a6 --- /dev/null +++ b/.changelog/1265.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +r2_bucket: add support for getting a bucket +``` + +```release-note:breaking-change +r2_bucket: change creation time from string to *time.Time +``` diff --git a/.changelog/1266.txt b/.changelog/1266.txt new file mode 100644 index 0000000000..749e3b8c8a --- /dev/null +++ b/.changelog/1266.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dns: add support for importing and exporting DNS records using BIND file configurations +``` diff --git a/.changelog/1267.txt b/.changelog/1267.txt new file mode 100644 index 0000000000..33e3526ec0 --- /dev/null +++ b/.changelog/1267.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +turnstile: add support for turnstile +``` diff --git a/.changelog/1268.txt b/.changelog/1268.txt new file mode 100644 index 0000000000..586bac29d8 --- /dev/null +++ b/.changelog/1268.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +device_posture_rule: add input fields tanium, intune and kolide +``` \ No newline at end of file diff --git a/.changelog/1269.txt b/.changelog/1269.txt new file mode 100644 index 0000000000..686ac6a9af --- /dev/null +++ b/.changelog/1269.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps dependabot/fetch-metadata from 1.3.6 to 1.4.0 +``` diff --git a/.changelog/1270.txt b/.changelog/1270.txt new file mode 100644 index 0000000000..4c09f1f51d --- /dev/null +++ b/.changelog/1270.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data localization: add support for regional hostnames API +``` diff --git a/.changelog/1271.txt b/.changelog/1271.txt new file mode 100644 index 0000000000..664212a1e7 --- /dev/null +++ b/.changelog/1271.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +certificate_packs: add `Status` field to indicate the status of certificate pack +``` diff --git a/.changelog/1272.txt b/.changelog/1272.txt new file mode 100644 index 0000000000..dfa3888383 --- /dev/null +++ b/.changelog/1272.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +logpush: add support for max upload parameters +``` diff --git a/.changelog/1274.txt b/.changelog/1274.txt new file mode 100644 index 0000000000..51deac303d --- /dev/null +++ b/.changelog/1274.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.25.1 to 2.25.3 +``` diff --git a/.changelog/1275.txt b/.changelog/1275.txt new file mode 100644 index 0000000000..718b80065f --- /dev/null +++ b/.changelog/1275.txt @@ -0,0 +1,3 @@ +```release-note:bug +rulesets: allow `PreserveQueryString` to be nullable +``` diff --git a/.changelog/1276.txt b/.changelog/1276.txt new file mode 100644 index 0000000000..713898c8db --- /dev/null +++ b/.changelog/1276.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +waiting_room: add support for zone-level settings +``` diff --git a/.changelog/1278.txt b/.changelog/1278.txt new file mode 100644 index 0000000000..64e605cf14 --- /dev/null +++ b/.changelog/1278.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +zone: Added `GetCacheReserve` and `UpdateacheReserve` to allow setting Cache Reserve for a zone. +``` diff --git a/.changelog/1279.txt b/.changelog/1279.txt new file mode 100644 index 0000000000..08cd4cab05 --- /dev/null +++ b/.changelog/1279.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +pages: add support for Smart Placement. Added `Placement` in `PagesProjectDeploymentConfigEnvironment`. +``` + +```release-note:enhancement +workers: add support for Smart Placement. Added `Placement` in `CreateWorkerParams`. +``` diff --git a/.changelog/1280.txt b/.changelog/1280.txt new file mode 100644 index 0000000000..7f690ce831 --- /dev/null +++ b/.changelog/1280.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.9.0 to 0.10.0 +``` diff --git a/.changelog/1284.txt b/.changelog/1284.txt new file mode 100644 index 0000000000..0da545211a --- /dev/null +++ b/.changelog/1284.txt @@ -0,0 +1,3 @@ +```release-note:bug +turnstile: remove `SiteKey`/`Secret` being sent in update request body +``` diff --git a/.changelog/1285.txt b/.changelog/1285.txt new file mode 100644 index 0000000000..75140aed1b --- /dev/null +++ b/.changelog/1285.txt @@ -0,0 +1,3 @@ +```release-note:bug +turnstile: remove `SiteKey` being sent in rotate secret's request body +``` diff --git a/.changelog/1286.txt b/.changelog/1286.txt new file mode 100644 index 0000000000..380cc29730 --- /dev/null +++ b/.changelog/1286.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/stretchr/testify from 1.8.2 to 1.8.3 +``` diff --git a/.changelog/1287.txt b/.changelog/1287.txt new file mode 100644 index 0000000000..3e78c82109 --- /dev/null +++ b/.changelog/1287.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps dependabot/fetch-metadata from 1.4.0 to 1.5.0 +``` diff --git a/.changelog/1288.txt b/.changelog/1288.txt new file mode 100644 index 0000000000..7401823441 --- /dev/null +++ b/.changelog/1288.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +lists: add support for hostname and ASN lists. +``` \ No newline at end of file diff --git a/.changelog/1289.txt b/.changelog/1289.txt new file mode 100644 index 0000000000..1c9e25e7aa --- /dev/null +++ b/.changelog/1289.txt @@ -0,0 +1,3 @@ +```release-note:bug +flarectl/dns: ensure MX priority value is dereferenced +``` diff --git a/.changelog/1290.txt b/.changelog/1290.txt new file mode 100644 index 0000000000..70224ff8f5 --- /dev/null +++ b/.changelog/1290.txt @@ -0,0 +1,3 @@ +```release-note:bug +dns: fix MX record priority not set by UpdateDNSRecord +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 13e93599b2..b71eecf0ee 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -34,6 +34,7 @@ What sort of change does your code introduce/modify? - [ ] I have updated the documentation accordingly. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. -- [ ] This change is using publicly documented (api.cloudflare.com or developers.cloudflare.com) and stable APIs. +- [ ] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) + and relies on stable APIs. [1]: https://help.github.com/articles/closing-issues-using-keywords/ diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index dba1a7f545..4b3616de73 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -3,16 +3,13 @@ on: [pull_request_target] jobs: changelog-check: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: ^1.17 + go-version-file: 'internal/tools/go.mod' - run: go generate -tags tools internal/tools/tools.go - run: go run cmd/changelog-check/main.go ${{ github.event.pull_request.number }} working-directory: ./internal/tools diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7dfe116df9..749581b54f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,15 +4,15 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - name: Checkout code uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' - uses: actions/cache@v3 with: path: ~/go/pkg/mod - key: ${{ runner.os }}-go1.18-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }} - name: Run coverage run: go test ./... -coverprofile=coverage.txt -covermode=atomic - name: Upload coverage to Codecov diff --git a/.github/workflows/dependabot-changelog.yml b/.github/workflows/dependabot-changelog.yml index 88e6036bc4..2afaac2ddb 100644 --- a/.github/workflows/dependabot-changelog.yml +++ b/.github/workflows/dependabot-changelog.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Fetch dependabot metadata id: dependabot-metadata - uses: dependabot/fetch-metadata@v1.3.5 + uses: dependabot/fetch-metadata@v1.5.0 - uses: actions/checkout@v3 - run: | gh pr checkout $PR_URL diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index 341cf5c9d9..691bdf3cf6 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -8,12 +8,12 @@ jobs: if: github.event.pull_request.merged || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-go@v4 + with: + go-version-file: 'internal/tools/go.mod' - run: go generate -tags tools internal/tools/tools.go - run: ./scripts/generate-changelog.sh - run: | diff --git a/.github/workflows/lock-released-issues.yml b/.github/workflows/lock-released-issues.yml new file mode 100644 index 0000000000..e165223f1a --- /dev/null +++ b/.github/workflows/lock-released-issues.yml @@ -0,0 +1,29 @@ +name: Lock released issues and PRs + +on: + workflow_dispatch: + inputs: + issue_list: + description: Comma seperated (no spaces) list of issues/PRs to lock + required: true + +permissions: + issues: write + pull-requests: write + +jobs: + lock-closed-issues: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: | + IFS=',' read -r -a issues <<< "$ISSUES" + for element in "${issues[@]}" + do + echo "Locking $element" + echo "no" | gh pr lock -r resolved $element || true + echo "no" | gh issue lock -r resolved $element || true + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUES: ${{ github.event.inputs.issue_list }} diff --git a/.github/workflows/milestone-closed.yml b/.github/workflows/milestone-closed.yml index 5f1a045685..c29167a0d7 100644 --- a/.github/workflows/milestone-closed.yml +++ b/.github/workflows/milestone-closed.yml @@ -11,6 +11,8 @@ permissions: jobs: comment-on-closed-milestone: runs-on: ubuntu-latest + outputs: + ids: ${{ steps.milestone-comment.outputs.ids }} steps: - uses: bflad/action-milestone-comment@v1 with: @@ -18,3 +20,20 @@ jobs: This functionality has been released in [${{ github.event.milestone.title }}](https://github.com/${{ github.repository }}/releases/tag/${{ github.event.milestone.title }}). For further feature requests or bug reports with this functionality, please create a [new GitHub issue](https://github.com/${{ github.repository }}/issues/new/choose) following the template. Thank you! + + lock-closed-issues: + runs-on: ubuntu-latest + needs: comment-on-closed-milestone + steps: + - uses: actions/checkout@v3 + - run: | + IFS=',' read -r -a issues <<< "$ISSUES" + for element in "${issues[@]}" + do + echo "Locking $element" + echo "no" | gh pr lock -r resolved $element || true + echo "no" | gh issue lock -r resolved $element || true + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUES: ${{ needs.comment-on-closed-milestone.outputs.ids }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d6a468159..d93438671a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,11 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: - go-version: ^1.17 + go-version-file: 'go.mod' - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3.2.0 + uses: goreleaser/goreleaser-action@v4.2.0 with: version: latest args: release --rm-dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7cfdd13f0..142f386590 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,11 +4,10 @@ jobs: test: strategy: matrix: - go-version: [1.17, 1.18, 1.19] - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} + go-version: ["1.18", "1.19", "1.20"] + runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code diff --git a/.gitignore b/.gitignore index 5acd30652d..0953e13b27 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ cmd/flarectl/dist/ cmd/flarectl/flarectl cmd/flarectl/flarectl.exe +.flox/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 594b3bd880..8f820bfd3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,296 @@ -## 0.56.0 (Unreleased) +## 0.68.0 (Unreleased) + +BREAKING CHANGES: + +* r2_bucket: change creation time from string to *time.Time ([#1265](https://github.com/cloudflare/cloudflare-go/issues/1265)) + +ENHANCEMENTS: + +* lists: add support for hostname and ASN lists. ([#1288](https://github.com/cloudflare/cloudflare-go/issues/1288)) +* pages: add support for Smart Placement. Added `Placement` in `PagesProjectDeploymentConfigEnvironment`. ([#1279](https://github.com/cloudflare/cloudflare-go/issues/1279)) +* r2_bucket: add support for getting a bucket ([#1265](https://github.com/cloudflare/cloudflare-go/issues/1265)) +* workers: add support for Smart Placement. Added `Placement` in `CreateWorkerParams`. ([#1279](https://github.com/cloudflare/cloudflare-go/issues/1279)) +* zone: Added `GetCacheReserve` and `UpdateacheReserve` to allow setting Cache Reserve for a zone. ([#1278](https://github.com/cloudflare/cloudflare-go/issues/1278)) + +BUG FIXES: + +* dns: fix MX record priority not set by UpdateDNSRecord ([#1290](https://github.com/cloudflare/cloudflare-go/issues/1290)) +* flarectl/dns: ensure MX priority value is dereferenced ([#1289](https://github.com/cloudflare/cloudflare-go/issues/1289)) +* turnstile: remove `SiteKey` being sent in rotate secret's request body ([#1285](https://github.com/cloudflare/cloudflare-go/issues/1285)) +* turnstile: remove `SiteKey`/`Secret` being sent in update request body ([#1284](https://github.com/cloudflare/cloudflare-go/issues/1284)) + +DEPENDENCIES: + +* deps: bumps dependabot/fetch-metadata from 1.4.0 to 1.5.0 ([#1287](https://github.com/cloudflare/cloudflare-go/issues/1287)) +* deps: bumps github.com/stretchr/testify from 1.8.2 to 1.8.3 ([#1286](https://github.com/cloudflare/cloudflare-go/issues/1286)) + +## 0.67.0 (May 10th, 2023) + +NOTES: + +* dns_firewall: The `OriginIPs` field has been renamed to `UpstreamIPs`. ([#1246](https://github.com/cloudflare/cloudflare-go/issues/1246)) + +ENHANCEMENTS: + +* device_posture_rule: add input fields tanium, intune and kolide ([#1268](https://github.com/cloudflare/cloudflare-go/issues/1268)) +* waiting_room: add support for zone-level settings ([#1276](https://github.com/cloudflare/cloudflare-go/issues/1276)) + +BUG FIXES: + +* rulesets: allow `PreserveQueryString` to be nullable ([#1275](https://github.com/cloudflare/cloudflare-go/issues/1275)) + +DEPENDENCIES: + +* deps: bumps github.com/urfave/cli/v2 from 2.25.1 to 2.25.3 ([#1274](https://github.com/cloudflare/cloudflare-go/issues/1274)) +* deps: bumps golang.org/x/net from 0.9.0 to 0.10.0 ([#1280](https://github.com/cloudflare/cloudflare-go/issues/1280)) + +## 0.66.0 (26th April, 2023) + +ENHANCEMENTS: + +* access_application: Add `path_cookie_attribute` app setting ([#1223](https://github.com/cloudflare/cloudflare-go/issues/1223)) +* certificate_packs: add `Status` field to indicate the status of certificate pack ([#1271](https://github.com/cloudflare/cloudflare-go/issues/1271)) +* data localization: add support for regional hostnames API ([#1270](https://github.com/cloudflare/cloudflare-go/issues/1270)) +* dns: add support for importing and exporting DNS records using BIND file configurations ([#1266](https://github.com/cloudflare/cloudflare-go/issues/1266)) +* logpush: add support for max upload parameters ([#1272](https://github.com/cloudflare/cloudflare-go/issues/1272)) +* turnstile: add support for turnstile ([#1267](https://github.com/cloudflare/cloudflare-go/issues/1267)) + +DEPENDENCIES: + +* deps: bumps dependabot/fetch-metadata from 1.3.6 to 1.4.0 ([#1269](https://github.com/cloudflare/cloudflare-go/issues/1269)) + +## 0.65.0 (12th April, 2023) + +ENHANCEMENTS: + +* access: Add `auto_redirect_to_identity` flag to Access organizations ([#1260](https://github.com/cloudflare/cloudflare-go/issues/1260)) +* access: Add `isolation_required` flag to Access policies ([#1258](https://github.com/cloudflare/cloudflare-go/issues/1258)) +* rulesets: add support for add operation to HTTP header configuration ([#1253](https://github.com/cloudflare/cloudflare-go/issues/1253)) +* rulesets: add support for the `compress_response` action ([#1261](https://github.com/cloudflare/cloudflare-go/issues/1261)) +* rulesets: add support for the `http_response_compression` phase ([#1261](https://github.com/cloudflare/cloudflare-go/issues/1261)) + +DEPENDENCIES: + +* deps: bumps golang.org/x/net from 0.8.0 to 0.9.0 ([#1263](https://github.com/cloudflare/cloudflare-go/issues/1263)) + +## 0.64.0 (29th March, 2023) + +BREAKING CHANGES: + +* dns: Changed Create/UpdateDNSRecord method signatures to return (DNSRecord, error) ([#1243](https://github.com/cloudflare/cloudflare-go/issues/1243)) +* zone: `UpdateZoneSingleSetting` has been renamed to `UpdateZoneSetting` and updated method signature inline with our expected conventions ([#1251](https://github.com/cloudflare/cloudflare-go/issues/1251)) +* zone: `ZoneSingleSetting` has been renamed to `GetZoneSetting` and updated method signature inline with our expected conventions ([#1251](https://github.com/cloudflare/cloudflare-go/issues/1251)) + +ENHANCEMENTS: + +* access_identity_provider: add `claims` and `scopes` fields ([#1237](https://github.com/cloudflare/cloudflare-go/issues/1237)) +* access_identity_provider: add scim_config field ([#1178](https://github.com/cloudflare/cloudflare-go/issues/1178)) +* devices_policy: update `Mode` field to use new `ServiceMode` string type with explicit const service mode values ([#1249](https://github.com/cloudflare/cloudflare-go/issues/1249)) +* ssl: make `GeoRestrictions` a pointer inside of ZoneCustomSSL ([#1244](https://github.com/cloudflare/cloudflare-go/issues/1244)) +* zone: `GetZoneSetting` and `UpdateZoneSetting` now allow configuring the path for where a setting resides instead of assuming `settings` ([#1251](https://github.com/cloudflare/cloudflare-go/issues/1251)) + +BUG FIXES: + +* teams_rules: `AllowChildBypass` changes from a `bool` to `*bool` ([#1242](https://github.com/cloudflare/cloudflare-go/issues/1242)) +* teams_rules: `BypassParentRule` changes from a `bool` to `*bool` ([#1242](https://github.com/cloudflare/cloudflare-go/issues/1242)) +* tunnel: Fix 'CreateTunnel' for tunnels using config_src ([#1238](https://github.com/cloudflare/cloudflare-go/issues/1238)) + +DEPENDENCIES: + +* deps: bumps actions/setup-go from 3 to 4 ([#1236](https://github.com/cloudflare/cloudflare-go/issues/1236)) +* deps: bumps github.com/urfave/cli/v2 from 2.25.0 to 2.25.1 ([#1250](https://github.com/cloudflare/cloudflare-go/issues/1250)) + +## 0.63.0 (15th March, 2023) + +BREAKING CHANGES: + +* tunnel: renamed `Tunnel` to `GetTunnel` ([#1227](https://github.com/cloudflare/cloudflare-go/issues/1227)) +* tunnel: renamed `Tunnels` to `ListTunnels` ([#1227](https://github.com/cloudflare/cloudflare-go/issues/1227)) + +ENHANCEMENTS: + +* access_organization: add ui_read_only_toggle_reason field ([#1181](https://github.com/cloudflare/cloudflare-go/issues/1181)) +* added audit_ssh to gateway actions, updated gateway rule settings ([#1226](https://github.com/cloudflare/cloudflare-go/issues/1226)) +* addressing: Add `Address Map` support ([#1232](https://github.com/cloudflare/cloudflare-go/issues/1232)) +* teams_account: add support for `check_disks` ([#1197](https://github.com/cloudflare/cloudflare-go/issues/1197)) +* tunnel: updated parameters to latest API docs ([#1227](https://github.com/cloudflare/cloudflare-go/issues/1227)) + +DEPENDENCIES: + +* deps: bumps github.com/urfave/cli/v2 from 2.24.4 to 2.25.0 ([#1229](https://github.com/cloudflare/cloudflare-go/issues/1229)) +* deps: bumps golang.org/x/net from 0.7.0 to 0.8.0 ([#1228](https://github.com/cloudflare/cloudflare-go/issues/1228)) + +## 0.62.0 (1st March, 2023) + +ENHANCEMENTS: + +* dex_test: add CRUD functionality for DEX test configurations ([#1209](https://github.com/cloudflare/cloudflare-go/issues/1209)) +* dlp: Adds support for partial payload logging ([#1212](https://github.com/cloudflare/cloudflare-go/issues/1212)) +* teams_accounts: Add new root_certificate_installation_enabled field ([#1208](https://github.com/cloudflare/cloudflare-go/issues/1208)) +* teams_rules: Add `untrusted_cert` rule setting ([#1214](https://github.com/cloudflare/cloudflare-go/issues/1214)) +* tunnels: automatically paginate `ListTunnels` ([#1206](https://github.com/cloudflare/cloudflare-go/issues/1206)) + +BUG FIXES: + +* dex_test: use dex test types and json struct mappings instead of managed networks ([#1213](https://github.com/cloudflare/cloudflare-go/issues/1213)) +* dns: dont reuse DNSListResponse when using pagination to avoid Proxied pointer overwrite ([#1222](https://github.com/cloudflare/cloudflare-go/issues/1222)) + +DEPENDENCIES: + +* deps: bumps github.com/stretchr/testify from 1.8.1 to 1.8.2 ([#1220](https://github.com/cloudflare/cloudflare-go/issues/1220)) +* deps: bumps github.com/urfave/cli/v2 from 2.24.3 to 2.24.4 ([#1210](https://github.com/cloudflare/cloudflare-go/issues/1210)) +* deps: bumps golang.org/x/net from 0.0.0-20220722155237-a158d28d115b to 0.7.0 ([#1218](https://github.com/cloudflare/cloudflare-go/issues/1218)) +* deps: bumps golang.org/x/net from 0.0.0-20220722155237-a158d28d115b to 0.7.0 ([#1219](https://github.com/cloudflare/cloudflare-go/issues/1219)) +* deps: bumps golang.org/x/text from 0.3.7 to 0.3.8 ([#1215](https://github.com/cloudflare/cloudflare-go/issues/1215)) +* deps: bumps golang.org/x/text from 0.3.7 to 0.3.8 ([#1216](https://github.com/cloudflare/cloudflare-go/issues/1216)) +* deps: bumps golang.org/x/time from 0.0.0-20220224211638-0e9765cccd65 to 0.3.0 ([#1217](https://github.com/cloudflare/cloudflare-go/issues/1217)) + +## 0.61.0 (15th February, 2023) + +ENHANCEMENTS: + +* cloudflare: make it clearer when we hit a server error and to retry later ([#1207](https://github.com/cloudflare/cloudflare-go/issues/1207)) +* devices_policy: Add new exclude_office_ips field to policy ([#1205](https://github.com/cloudflare/cloudflare-go/issues/1205)) +* dlp_profile: Use int rather than uint for allowed_match_count field ([#1200](https://github.com/cloudflare/cloudflare-go/issues/1200)) + +BUG FIXES: + +* dns: always send `tags` to allow clearing ([#1196](https://github.com/cloudflare/cloudflare-go/issues/1196)) +* stream: renamed `RequiredSignedURLs` to `RequireSignedURLs` ([#1202](https://github.com/cloudflare/cloudflare-go/issues/1202)) + +DEPENDENCIES: + +* deps: bumps github.com/urfave/cli/v2 from 2.24.2 to 2.24.3 ([#1199](https://github.com/cloudflare/cloudflare-go/issues/1199)) + +## 0.60.0 (1st February, 2023) + +BREAKING CHANGES: + +* queues: UpdateQueue has been updated to match the API and now correctly updates a Queue's name ([#1188](https://github.com/cloudflare/cloudflare-go/issues/1188)) + +ENHANCEMENTS: + +* dlp_profile: Add new allowed_match_count field to profiles ([#1193](https://github.com/cloudflare/cloudflare-go/issues/1193)) +* dns: allow sending empty strings to remove comments ([#1195](https://github.com/cloudflare/cloudflare-go/issues/1195)) +* magic_transit_ipsec_tunnel: makes customer endpoint an optional field for ipsec tunnel creation ([#1185](https://github.com/cloudflare/cloudflare-go/issues/1185)) +* rulesets: add support for `score_per_period` and `score_response_header_name` ([#1183](https://github.com/cloudflare/cloudflare-go/issues/1183)) + +DEPENDENCIES: + +* deps: bumps dependabot/fetch-metadata from 1.3.5 to 1.3.6 ([#1184](https://github.com/cloudflare/cloudflare-go/issues/1184)) +* deps: bumps github.com/urfave/cli/v2 from 2.23.7 to 2.24.1 ([#1180](https://github.com/cloudflare/cloudflare-go/issues/1180)) +* deps: bumps github.com/urfave/cli/v2 from 2.24.1 to 2.24.2 ([#1191](https://github.com/cloudflare/cloudflare-go/issues/1191)) +* deps: bumps goreleaser/goreleaser-action from 4.1.0 to 4.2.0 ([#1192](https://github.com/cloudflare/cloudflare-go/issues/1192)) + +## 0.59.0 (January 18th, 2023) + +BREAKING CHANGES: + +* dns: remove these read-only fields from `UpdateDNSRecordParams`: `CreatedOn`, `ModifiedOn`, `Meta`, `ZoneID`, `ZoneName`, `Proxiable`, and `Locked` ([#1170](https://github.com/cloudflare/cloudflare-go/issues/1170)) +* dns: the fields `CreatedOn` and `ModifiedOn` are removed from `ListDNSRecordsParams` ([#1173](https://github.com/cloudflare/cloudflare-go/issues/1173)) + +NOTES: + +* dns: remove additional lookup from `Update` operations when `Name` or `Type` was omitted ([#1170](https://github.com/cloudflare/cloudflare-go/issues/1170)) + +ENHANCEMENTS: + +* access_organization: add user_seat_expiration_inactive_time field ([#1159](https://github.com/cloudflare/cloudflare-go/issues/1159)) +* dns: `GetDNSRecord`, `UpdateDNSRecord` and `DeleteDNSRecord` now return the new, dedicated error `ErrMissingDNSRecordID` when an empty DNS record ID is given. ([#1174](https://github.com/cloudflare/cloudflare-go/issues/1174)) +* dns: the URL parameter `tag-match` for listing DNS records is now supported as the field `TagMatch` in `ListDNSRecordsParams` ([#1173](https://github.com/cloudflare/cloudflare-go/issues/1173)) +* dns: update default `per_page` attribute to 100 records ([#1171](https://github.com/cloudflare/cloudflare-go/issues/1171)) +* teams_rules: adds support for Egress Policies ([#1142](https://github.com/cloudflare/cloudflare-go/issues/1142)) +* workers: Add support for compatibility_date and compatibility_flags when upoading a worker script ([#1177](https://github.com/cloudflare/cloudflare-go/issues/1177)) +* workers: script upload now supports Queues bindings ([#1176](https://github.com/cloudflare/cloudflare-go/issues/1176)) + +BUG FIXES: + +* dns: don't send "priority" for list operations as it isn't supported and is only used for internal filtering ([#1167](https://github.com/cloudflare/cloudflare-go/issues/1167)) +* dns: the field `Tags` in `ListDNSRecordsParams` was not correctly serialized into URL queries ([#1173](https://github.com/cloudflare/cloudflare-go/issues/1173)) +* managednetworks: Update should be PUT ([#1172](https://github.com/cloudflare/cloudflare-go/issues/1172)) + +## 0.58.1 (January 5th, 2023) + +ENHANCEMENTS: + +* cloudflare: automatically redact sensitive values from HTTP interactions ([#1164](https://github.com/cloudflare/cloudflare-go/issues/1164)) + +## 0.58.0 (January 4th, 2023) + +BREAKING CHANGES: + +* dns: `DNSRecord` has been renamed to `GetDNSRecord` ([#1151](https://github.com/cloudflare/cloudflare-go/issues/1151)) +* dns: `DNSRecords` has been renamed to `ListDNSRecords` ([#1151](https://github.com/cloudflare/cloudflare-go/issues/1151)) +* dns: method signatures have been updated to align with the upcoming client conventions ([#1151](https://github.com/cloudflare/cloudflare-go/issues/1151)) +* origin_ca: renamed to `CreateOriginCertificate` to `CreateOriginCACertificate` ([#1161](https://github.com/cloudflare/cloudflare-go/issues/1161)) +* origin_ca: renamed to `OriginCARootCertificate` to `GetOriginCARootCertificate` ([#1161](https://github.com/cloudflare/cloudflare-go/issues/1161)) +* origin_ca: renamed to `OriginCertificate` to `GetOriginCACertificate` ([#1161](https://github.com/cloudflare/cloudflare-go/issues/1161)) +* origin_ca: renamed to `OriginCertificates` to `ListOriginCACertificates` ([#1161](https://github.com/cloudflare/cloudflare-go/issues/1161)) +* origin_ca: renamed to `RevokeOriginCertificate` to `RevokeOriginCACertificate` ([#1161](https://github.com/cloudflare/cloudflare-go/issues/1161)) + +ENHANCEMENTS: + +* dns: add support for tags and comments ([#1151](https://github.com/cloudflare/cloudflare-go/issues/1151)) +* mtls_certificate: add support for managing mTLS certificates and assocations ([#1150](https://github.com/cloudflare/cloudflare-go/issues/1150)) +* origin_ca: add support for using API keys, API tokens or API User service keys for interacting with Origin CA endpoints ([#1161](https://github.com/cloudflare/cloudflare-go/issues/1161)) +* workers: Add support for workers logpush enablement on script upload ([#1160](https://github.com/cloudflare/cloudflare-go/issues/1160)) + +BUG FIXES: + +* email_routing_destination: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* email_routing_rules: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* filter: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* firewall_rules: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* lockdown: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* queue: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* teams_list: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) +* workers_kv: use empty reponse struct on each page call ([#1156](https://github.com/cloudflare/cloudflare-go/issues/1156)) + +DEPENDENCIES: + +* deps: bumps github.com/hashicorp/go-retryablehttp from 0.7.1 to 0.7.2 ([#1162](https://github.com/cloudflare/cloudflare-go/issues/1162)) + +## 0.57.1 (December 23rd, 2022) + +ENHANCEMENTS: + +* tiered_cache: Add support for Tiered Caching interactions for setting Smart and Generic topologies ([#1149](https://github.com/cloudflare/cloudflare-go/issues/1149)) + +BUG FIXES: + +* workers: correctly set `body` value for non-ES module uploads ([#1155](https://github.com/cloudflare/cloudflare-go/issues/1155)) + +## 0.57.0 (December 22nd, 2022) + +BREAKING CHANGES: + +* workers: API operations now target account level resources instead of older zone level resources (these are a 1:1 now) ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers_bindings: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers_cron_triggers: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers_kv: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers_routes: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers_secrets: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers_tails: method signatures have been updated to align with the upcoming client conventions ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) + +NOTES: + +* workers: all worker methods have been split into product ownership(-ish) files ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) +* workers: all worker methods now require an explicit `ResourceContainer` for endpoints instead of relying on the globally defined `api.AccountID` ([#1137](https://github.com/cloudflare/cloudflare-go/issues/1137)) + +ENHANCEMENTS: + +* managed_networks: add CRUD functionality for managednetworks ([#1148](https://github.com/cloudflare/cloudflare-go/issues/1148)) + +DEPENDENCIES: + +* deps: bumps goreleaser/goreleaser-action from 3.2.0 to 4.1.0 ([#1146](https://github.com/cloudflare/cloudflare-go/issues/1146)) + +## 0.56.0 (December 5th, 2022) + +BREAKING CHANGES: + +* pages: Changed the type of EnvVars in PagesProjectDeploymentConfigEnvironment & PagesProjectDeployment in order to properly support secrets. ([#1136](https://github.com/cloudflare/cloudflare-go/issues/1136)) NOTES: @@ -6,8 +298,14 @@ NOTES: ENHANCEMENTS: +* cache_rules: add ignore option to query string struct ([#1140](https://github.com/cloudflare/cloudflare-go/issues/1140)) +* pages: Updates bindings and other Functions related propreties. Service bindings, secrets, fail open/close and usage model are all now supported. ([#1136](https://github.com/cloudflare/cloudflare-go/issues/1136)) * workers: Support for Workers Analytics Engine bindings ([#1133](https://github.com/cloudflare/cloudflare-go/issues/1133)) +DEPENDENCIES: + +* deps: bumps github.com/urfave/cli/v2 from 2.23.5 to 2.23.6 ([#1139](https://github.com/cloudflare/cloudflare-go/issues/1139)) + ## 0.55.0 (November 23th, 2022) BREAKING CHANGES: diff --git a/README.md b/README.md index 311bea03d7..dc93558467 100644 --- a/README.md +++ b/README.md @@ -21,37 +21,6 @@ A Go library for interacting with A command-line client, [flarectl](cmd/flarectl), is also available as part of this project. -## Features - -The current feature list includes: - -- [x] Cache purging -- [x] Cloudflare IPs -- [x] Custom hostnames -- [x] DNS Firewall -- [x] DNS Records -- [x] Firewall (partial) -- [x] Gateway Locations -- [x] [Keyless SSL](https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/) -- [x] [Load Balancing](https://blog.cloudflare.com/introducing-load-balancing-intelligent-failover-with-cloudflare/) -- [x] [Logpush Jobs](https://developers.cloudflare.com/logs/logpush/) -- [x] Magic Transit / Magic WAN -- [x] Notifications -- [ ] Organization Administration -- [x] [Origin CA](https://blog.cloudflare.com/universal-ssl-encryption-all-the-way-to-the-origin-for-free/) -- [x] [Railgun](https://www.cloudflare.com/railgun/) administration -- [x] Rate Limiting -- [x] User Administration (partial) -- [x] Web Application Firewall (WAF) -- [x] Workers KV -- [x] Zone cache settings -- [x] Zone Lockdown and User-Agent Block rules -- [x] Zones -- [x] Managed Headers - -Pull Requests are welcome, but please open an issue (or comment in an existing -issue) to discuss any non-trivial changes before submitting code. - ## Installation You need a working Go environment. We officially support only currently supported Go versions according to [Go project's release policy](https://go.dev/doc/devel/release#policy). @@ -93,20 +62,6 @@ func main() { } // Print user details fmt.Println(u) - - // Fetch the zone ID - id, err := api.ZoneIDByName("example.com") // Assuming example.com exists in your Cloudflare account already - if err != nil { - log.Fatal(err) - } - - // Fetch zone details - zone, err := api.ZoneDetails(ctx, id) - if err != nil { - log.Fatal(err) - } - // Print zone details - fmt.Println(zone) } ``` @@ -120,6 +75,11 @@ This library is starting to ship with experimental improvements that are not yet ready for production but will be introduced before the next major version. See [experimental README](/docs/experimental.md) for full details. -# License +## Contributing + +Pull Requests are welcome, but please open an issue (or comment in an existing +issue) to discuss any non-trivial changes before submitting code. + +## License BSD licensed. See the [LICENSE](LICENSE) file for details. diff --git a/access_application.go b/access_application.go index 9b5a6aac9f..e627fca27c 100644 --- a/access_application.go +++ b/access_application.go @@ -48,6 +48,7 @@ type AccessApplication struct { EnableBindingCookie *bool `json:"enable_binding_cookie,omitempty"` HttpOnlyCookieAttribute *bool `json:"http_only_cookie_attribute,omitempty"` ServiceAuth401Redirect *bool `json:"service_auth_401_redirect,omitempty"` + PathCookieAttribute *bool `json:"path_cookie_attribute,omitempty"` } type AccessApplicationGatewayRule struct { diff --git a/access_application_test.go b/access_application_test.go index 608bd77e4d..ce488347d2 100644 --- a/access_application_test.go +++ b/access_application_test.go @@ -41,7 +41,8 @@ func TestAccessApplications(t *testing.T) { "logo_url": "https://www.example.com/example.png", "skip_interstitial": true, "app_launcher_visible": true, - "service_auth_401_redirect": true + "service_auth_401_redirect": true, + "path_cookie_attribute": true } ], "result_info": { @@ -77,6 +78,7 @@ func TestAccessApplications(t *testing.T) { HttpOnlyCookieAttribute: BoolPtr(true), LogoURL: "https://www.example.com/example.png", SkipInterstitial: BoolPtr(true), + PathCookieAttribute: BoolPtr(true), }} mux.HandleFunc("/accounts/"+testAccountID+"/access/apps", handler) @@ -125,7 +127,8 @@ func TestAccessApplication(t *testing.T) { "skip_interstitial": true, "app_launcher_visible": true, "service_auth_401_redirect": true, - "http_only_cookie_attribute": false + "http_only_cookie_attribute": false, + "path_cookie_attribute": false } } `) @@ -153,6 +156,7 @@ func TestAccessApplication(t *testing.T) { LogoURL: "https://www.example.com/example.png", SkipInterstitial: BoolPtr(true), HttpOnlyCookieAttribute: BoolPtr(false), + PathCookieAttribute: BoolPtr(false), } mux.HandleFunc("/accounts/"+testAccountID+"/access/apps/480f4f69-1a28-4fdd-9240-1ed29f0ac1db", handler) diff --git a/access_audit_log.go b/access_audit_log.go index 1627914d83..bb488f6348 100644 --- a/access_audit_log.go +++ b/access_audit_log.go @@ -74,11 +74,11 @@ func (a AccessAuditLogFilterOptions) Encode() string { } if a.Since != nil { - v.Set("since", (*a.Since).Format(time.RFC3339)) + v.Set("since", a.Since.Format(time.RFC3339)) } if a.Until != nil { - v.Set("until", (*a.Until).Format(time.RFC3339)) + v.Set("until", a.Until.Format(time.RFC3339)) } return v.Encode() diff --git a/access_identity_provider.go b/access_identity_provider.go index fb986b4b23..4d21ad38a5 100644 --- a/access_identity_provider.go +++ b/access_identity_provider.go @@ -9,10 +9,11 @@ import ( // AccessIdentityProvider is the structure of the provider object. type AccessIdentityProvider struct { - ID string `json:"id,omitempty"` - Name string `json:"name"` - Type string `json:"type"` - Config AccessIdentityProviderConfiguration `json:"config"` + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Config AccessIdentityProviderConfiguration `json:"config"` + ScimConfig AccessIdentityProviderScimConfiguration `json:"scim_config"` } // AccessIdentityProviderConfiguration is the combined structure of *all* @@ -30,6 +31,8 @@ type AccessIdentityProviderConfiguration struct { CertsURL string `json:"certs_url,omitempty"` ClientID string `json:"client_id,omitempty"` ClientSecret string `json:"client_secret,omitempty"` + Claims []string `json:"claims,omitempty"` + Scopes []string `json:"scopes,omitempty"` DirectoryID string `json:"directory_id,omitempty"` EmailAttributeName string `json:"email_attribute_name,omitempty"` IdpPublicCert string `json:"idp_public_cert,omitempty"` @@ -44,6 +47,14 @@ type AccessIdentityProviderConfiguration struct { PKCEEnabled *bool `json:"pkce_enabled,omitempty"` } +type AccessIdentityProviderScimConfiguration struct { + Enabled bool `json:"enabled,omitempty"` + Secret string `json:"secret,omitempty"` + UserDeprovision bool `json:"user_deprovision,omitempty"` + SeatDeprovision bool `json:"seat_deprovision,omitempty"` + GroupMemberDeprovision bool `json:"group_member_deprovision,omitempty"` +} + // AccessIdentityProvidersListResponse is the API response for multiple // Access Identity Providers. type AccessIdentityProvidersListResponse struct { diff --git a/access_organization.go b/access_organization.go index dbebdca7ea..ff2fef9ded 100644 --- a/access_organization.go +++ b/access_organization.go @@ -10,12 +10,15 @@ import ( // AccessOrganization represents an Access organization. type AccessOrganization struct { - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Name string `json:"name"` - AuthDomain string `json:"auth_domain"` - LoginDesign AccessOrganizationLoginDesign `json:"login_design"` - IsUIReadOnly *bool `json:"is_ui_read_only,omitempty"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Name string `json:"name"` + AuthDomain string `json:"auth_domain"` + LoginDesign AccessOrganizationLoginDesign `json:"login_design"` + IsUIReadOnly *bool `json:"is_ui_read_only,omitempty"` + UIReadOnlyToggleReason string `json:"ui_read_only_toggle_reason,omitempty"` + UserSeatExpirationInactiveTime string `json:"user_seat_expiration_inactive_time,omitempty"` + AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` } // AccessOrganizationLoginDesign represents the login design options. diff --git a/access_organization_test.go b/access_organization_test.go index 0e1cede85d..3ea79970a4 100644 --- a/access_organization_test.go +++ b/access_organization_test.go @@ -27,6 +27,8 @@ func TestAccessOrganization(t *testing.T) { "name": "Widget Corps Internal Applications", "auth_domain": "test.cloudflareaccess.com", "is_ui_read_only": false, + "user_seat_expiration_inactive_time": "720h", + "auto_redirect_to_identity": true, "login_design": { "background_color": "#c5ed1b", "logo_path": "https://example.com/logo.png", @@ -54,7 +56,9 @@ func TestAccessOrganization(t *testing.T) { HeaderText: "Widget Corp", FooterText: "© Widget Corp", }, - IsUIReadOnly: BoolPtr(false), + IsUIReadOnly: BoolPtr(false), + UserSeatExpirationInactiveTime: "720h", + AutoRedirectToIdentity: BoolPtr(true), } mux.HandleFunc("/accounts/"+testAccountID+"/access/organizations", handler) @@ -161,7 +165,8 @@ func TestUpdateAccessOrganization(t *testing.T) { "header_text": "Widget Corp", "footer_text": "© Widget Corp" }, - "is_ui_read_only": false + "is_ui_read_only": false, + "ui_read_only_toggle_reason": "this is my reason" } } `) @@ -182,7 +187,8 @@ func TestUpdateAccessOrganization(t *testing.T) { HeaderText: "Widget Corp", FooterText: "© Widget Corp", }, - IsUIReadOnly: BoolPtr(false), + IsUIReadOnly: BoolPtr(false), + UIReadOnlyToggleReason: "this is my reason", } mux.HandleFunc("/accounts/"+testAccountID+"/access/organizations", handler) diff --git a/access_policy.go b/access_policy.go index abbbaf42d5..a7823f79fc 100644 --- a/access_policy.go +++ b/access_policy.go @@ -24,6 +24,7 @@ type AccessPolicy struct { UpdatedAt *time.Time `json:"updated_at"` Name string `json:"name"` + IsolationRequired *bool `json:"isolation_required,omitempty"` PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` ApprovalRequired *bool `json:"approval_required,omitempty"` diff --git a/access_policy_test.go b/access_policy_test.go index 8184b47f0e..1164b29d44 100644 --- a/access_policy_test.go +++ b/access_policy_test.go @@ -19,6 +19,7 @@ var ( updatedAt, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") expiresAt, _ = time.Parse(time.RFC3339, "2015-01-01T05:20:00.12345Z") + isolationRequired = true purposeJustificationRequired = true purposeJustificationPrompt = "Please provide a business reason for your need to access before continuing." approvalRequired = true @@ -39,6 +40,7 @@ var ( Require: []interface{}{ map[string]interface{}{"email": map[string]interface{}{"email": "test@example.com"}}, }, + IsolationRequired: &isolationRequired, PurposeJustificationRequired: &purposeJustificationRequired, ApprovalRequired: &approvalRequired, PurposeJustificationPrompt: &purposeJustificationPrompt, @@ -95,6 +97,7 @@ func TestAccessPolicies(t *testing.T) { } } ], + "isolation_required": true, "purpose_justification_required": true, "purpose_justification_prompt": "Please provide a business reason for your need to access before continuing.", "approval_required": true, @@ -176,6 +179,7 @@ func TestAccessPolicy(t *testing.T) { } } ], + "isolation_required": true, "purpose_justification_required": true, "purpose_justification_prompt": "Please provide a business reason for your need to access before continuing.", "approval_required": true, @@ -250,6 +254,7 @@ func TestCreateAccessPolicy(t *testing.T) { } } ], + "isolation_required": true, "purpose_justification_required": true, "purpose_justification_prompt": "Please provide a business reason for your need to access before continuing.", "approval_required": true, @@ -356,6 +361,7 @@ func TestUpdateAccessPolicy(t *testing.T) { } } ], + "isolation_required": true, "purpose_justification_required": true, "purpose_justification_prompt": "Please provide a business reason for your need to access before continuing.", "approval_required": true, diff --git a/addressing_address_map.go b/addressing_address_map.go new file mode 100644 index 0000000000..93f9f9687e --- /dev/null +++ b/addressing_address_map.go @@ -0,0 +1,284 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// AddressMap contains information about an address map. +type AddressMap struct { + ID string `json:"id"` + Description *string `json:"description,omitempty"` + DefaultSNI *string `json:"default_sni"` + Enabled *bool `json:"enabled"` + Deletable *bool `json:"can_delete"` + CanModifyIPs *bool `json:"can_modify_ips"` + Memberships []AddressMapMembership `json:"memberships"` + IPs []AddressMapIP `json:"ips"` + CreatedAt time.Time `json:"created_at"` + ModifiedAt time.Time `json:"modified_at"` +} + +type AddressMapIP struct { + IP string `json:"ip"` + CreatedAt time.Time `json:"created_at"` +} + +type AddressMapMembershipContainer struct { + Identifier string `json:"identifier"` + Kind AddressMapMembershipKind `json:"kind"` +} + +type AddressMapMembership struct { + Identifier string `json:"identifier"` + Kind AddressMapMembershipKind `json:"kind"` + Deletable *bool `json:"can_delete"` + CreatedAt time.Time `json:"created_at"` +} + +func (ammb *AddressMapMembershipContainer) URLFragment() string { + switch ammb.Kind { + case AddressMapMembershipAccount: + return fmt.Sprintf("accounts/%s", ammb.Identifier) + case AddressMapMembershipZone: + return fmt.Sprintf("zones/%s", ammb.Identifier) + default: + return fmt.Sprintf("%s/%s", ammb.Kind, ammb.Identifier) + } +} + +type AddressMapMembershipKind string + +const ( + AddressMapMembershipZone AddressMapMembershipKind = "zone" + AddressMapMembershipAccount AddressMapMembershipKind = "account" +) + +// ListAddressMapResponse contains a slice of address maps. +type ListAddressMapResponse struct { + Response + Result []AddressMap `json:"result"` +} + +// GetAddressMapResponse contains a specific address map's API Response. +type GetAddressMapResponse struct { + Response + Result AddressMap `json:"result"` +} + +// CreateAddressMapParams contains information about an address map to be created. +type CreateAddressMapParams struct { + Description *string `json:"description"` + Enabled *bool `json:"enabled"` + IPs []string `json:"ips"` + Memberships []AddressMapMembershipContainer `json:"memberships"` +} + +// UpdateAddressMapParams contains information about an address map to be updated. +type UpdateAddressMapParams struct { + ID string `json:"-"` + Description *string `json:"description,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + DefaultSNI *string `json:"default_sni,omitempty"` +} + +// AddressMapFilter contains filter parameters for finding a list of address maps. +type ListAddressMapsParams struct { + IP *string `url:"ip,omitempty"` + CIDR *string `url:"cidr,omitempty"` +} + +// CreateIPAddressToAddressMapParams contains request parameters to add/remove IP address to/from an address map. +type CreateIPAddressToAddressMapParams struct { + // ID represents the target address map for adding the IP address. + ID string + // The IP address. + IP string +} + +// CreateMembershipToAddressMapParams contains request parameters to add/remove membership from an address map. +type CreateMembershipToAddressMapParams struct { + // ID represents the target address map for adding the membershp. + ID string + Membership AddressMapMembershipContainer +} + +type DeleteMembershipFromAddressMapParams struct { + // ID represents the target address map for removing the IP address. + ID string + Membership AddressMapMembershipContainer +} + +type DeleteIPAddressFromAddressMapParams struct { + // ID represents the target address map for adding the membershp. + ID string + IP string +} + +// ListAddressMaps lists all address maps owned by the account. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-list-address-maps +func (api *API) ListAddressMaps(ctx context.Context, rc *ResourceContainer, params ListAddressMapsParams) ([]AddressMap, error) { + if rc.Level != AccountRouteLevel { + return []AddressMap{}, ErrRequiredAccountLevelResourceContainer + } + + uri := buildURI(fmt.Sprintf("/%s/addressing/address_maps", rc.URLFragment()), params) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []AddressMap{}, err + } + + result := ListAddressMapResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []AddressMap{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return result.Result, nil +} + +// CreateAddressMap creates a new address map under the account. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-create-address-map +func (api *API) CreateAddressMap(ctx context.Context, rc *ResourceContainer, params CreateAddressMapParams) (AddressMap, error) { + if rc.Level != AccountRouteLevel { + return AddressMap{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/addressing/address_maps", rc.URLFragment()) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return AddressMap{}, err + } + + result := GetAddressMapResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return AddressMap{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return result.Result, nil +} + +// GetAddressMap returns a specific address map. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-address-map-details +func (api *API) GetAddressMap(ctx context.Context, rc *ResourceContainer, id string) (AddressMap, error) { + if rc.Level != AccountRouteLevel { + return AddressMap{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s", rc.URLFragment(), id) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return AddressMap{}, err + } + + result := GetAddressMapResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return AddressMap{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return result.Result, nil +} + +// UpdateAddressMap modifies properties of an address map owned by the account. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-update-address-map +func (api *API) UpdateAddressMap(ctx context.Context, rc *ResourceContainer, params UpdateAddressMapParams) (AddressMap, error) { + if rc.Level != AccountRouteLevel { + return AddressMap{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s", rc.URLFragment(), params.ID) + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params) + if err != nil { + return AddressMap{}, err + } + + result := GetAddressMapResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return AddressMap{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return result.Result, nil +} + +// DeleteAddressMap deletes a particular address map owned by the account. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-delete-address-map +func (api *API) DeleteAddressMap(ctx context.Context, rc *ResourceContainer, id string) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s", rc.URLFragment(), id) + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + return err +} + +// CreateIPAddressToAddressMap adds an IP address from a prefix owned by the account to a particular address map. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-add-an-ip-to-an-address-map +func (api *API) CreateIPAddressToAddressMap(ctx context.Context, rc *ResourceContainer, params CreateIPAddressToAddressMapParams) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s/ips/%s", rc.URLFragment(), params.ID, params.IP) + _, err := api.makeRequestContext(ctx, http.MethodPut, uri, nil) + return err +} + +// DeleteIPAddressFromAddressMap removes an IP address from a particular address map. +// +// API reference: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-remove-an-ip-from-an-address-map +func (api *API) DeleteIPAddressFromAddressMap(ctx context.Context, rc *ResourceContainer, params DeleteIPAddressFromAddressMapParams) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s/ips/%s", rc.URLFragment(), params.ID, params.IP) + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + return err +} + +// CreateMembershipToAddressMap adds a zone/account as a member of a particular address map. +// +// API reference: +// - account: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-add-an-account-membership-to-an-address-map +// - zone: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-add-a-zone-membership-to-an-address-map +func (api *API) CreateMembershipToAddressMap(ctx context.Context, rc *ResourceContainer, params CreateMembershipToAddressMapParams) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer + } + + if params.Membership.Kind != AddressMapMembershipZone && params.Membership.Kind != AddressMapMembershipAccount { + return fmt.Errorf("requested membershp kind (%q) is not supported", params.Membership.Kind) + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s/%s", rc.URLFragment(), params.ID, params.Membership.URLFragment()) + _, err := api.makeRequestContext(ctx, http.MethodPut, uri, nil) + return err +} + +// DeleteMembershipFromAddressMap removes a zone/account as a member of a particular address map. +// +// API reference: +// - account: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-remove-an-account-membership-from-an-address-map +// - zone: https://developers.cloudflare.com/api/operations/ip-address-management-address-maps-remove-a-zone-membership-from-an-address-map +func (api *API) DeleteMembershipFromAddressMap(ctx context.Context, rc *ResourceContainer, params DeleteMembershipFromAddressMapParams) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer + } + + if params.Membership.Kind != AddressMapMembershipZone && params.Membership.Kind != AddressMapMembershipAccount { + return fmt.Errorf("requested membershp kind (%q) is not supported", params.Membership.Kind) + } + + uri := fmt.Sprintf("/%s/addressing/address_maps/%s/%s", rc.URLFragment(), params.ID, params.Membership.URLFragment()) + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + return err +} diff --git a/addressing_address_map_test.go b/addressing_address_map_test.go new file mode 100644 index 0000000000..0bbb3dd606 --- /dev/null +++ b/addressing_address_map_test.go @@ -0,0 +1,384 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + addressMapDesc = "My Ecommerce zones" + addressMapDefaultSNI = "*.example.com" +) + +func TestListAddressMap(t *testing.T) { + setup() + defer teardown() + + expectedIP := "127.0.0.1" + expectedCIDR := "127.0.0.1/24" + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + + actualIP := r.URL.Query().Get("ip") + assert.Equal(t, expectedIP, actualIP, "Expected ip %q, got %q", expectedIP, actualIP) + + actualCIDR := r.URL.Query().Get("cidr") + assert.Equal(t, expectedCIDR, actualCIDR, "Expected cidr %q, got %q", expectedCIDR, actualCIDR) + + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [{ + "id": "9a7806061c88ada191ed06f989cc3dac", + "description": "My Ecommerce zones", + "can_delete": true, + "can_modify_ips": true, + "default_sni": "*.example.com", + "created_at": "2023-01-01T05:20:00.12345Z", + "modified_at": "2023-01-05T05:20:00.12345Z", + "enabled": true, + "ips": [ + { + "ip": "192.0.2.1", + "created_at": "2023-01-02T05:20:00.12345Z" + } + ], + "memberships": [ + { + "kind": "zone", + "identifier": "01a7362d577a6c3019a474fd6f485823", + "can_delete": true, + "created_at": "2023-01-03T05:20:00.12345Z" + } + ] + }] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps", handler) + + createdAt, _ := time.Parse(time.RFC3339, "2023-01-01T05:20:00.12345Z") + modifiedAt, _ := time.Parse(time.RFC3339, "2023-01-05T05:20:00.12345Z") + ipCreatedAt, _ := time.Parse(time.RFC3339, "2023-01-02T05:20:00.12345Z") + membershipCreatedAt, _ := time.Parse(time.RFC3339, "2023-01-03T05:20:00.12345Z") + + want := []AddressMap{ + { + ID: "9a7806061c88ada191ed06f989cc3dac", + CreatedAt: createdAt, + ModifiedAt: modifiedAt, + Description: &addressMapDesc, + Deletable: BoolPtr(true), + CanModifyIPs: BoolPtr(true), + DefaultSNI: &addressMapDefaultSNI, + Enabled: BoolPtr(true), + IPs: []AddressMapIP{{"192.0.2.1", ipCreatedAt}}, + Memberships: []AddressMapMembership{{ + Identifier: "01a7362d577a6c3019a474fd6f485823", + Kind: AddressMapMembershipZone, + Deletable: BoolPtr(true), + CreatedAt: membershipCreatedAt, + }}, + }, + } + + actual, err := client.ListAddressMaps(context.Background(), AccountIdentifier(testAccountID), ListAddressMapsParams{&expectedIP, &expectedCIDR}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetAddressMap(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "9a7806061c88ada191ed06f989cc3dac", + "description": "My Ecommerce zones", + "can_delete": true, + "can_modify_ips": true, + "default_sni": "*.example.com", + "created_at": "2023-01-01T05:20:00.12345Z", + "modified_at": "2023-01-05T05:20:00.12345Z", + "enabled": true, + "ips": [ + { + "ip": "192.0.2.1", + "created_at": "2023-01-02T05:20:00.12345Z" + } + ], + "memberships": [ + { + "kind": "zone", + "identifier": "01a7362d577a6c3019a474fd6f485823", + "can_delete": true, + "created_at": "2023-01-03T05:20:00.12345Z" + } + ] + } + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac", handler) + + createdAt, _ := time.Parse(time.RFC3339, "2023-01-01T05:20:00.12345Z") + modifiedAt, _ := time.Parse(time.RFC3339, "2023-01-05T05:20:00.12345Z") + ipCreatedAt, _ := time.Parse(time.RFC3339, "2023-01-02T05:20:00.12345Z") + membershipCreatedAt, _ := time.Parse(time.RFC3339, "2023-01-03T05:20:00.12345Z") + + want := AddressMap{ + ID: "9a7806061c88ada191ed06f989cc3dac", + CreatedAt: createdAt, + ModifiedAt: modifiedAt, + Description: &addressMapDesc, + Deletable: BoolPtr(true), + CanModifyIPs: BoolPtr(true), + DefaultSNI: &addressMapDefaultSNI, + Enabled: BoolPtr(true), + IPs: []AddressMapIP{{"192.0.2.1", ipCreatedAt}}, + Memberships: []AddressMapMembership{{ + Identifier: "01a7362d577a6c3019a474fd6f485823", + Kind: AddressMapMembershipZone, + Deletable: BoolPtr(true), + CreatedAt: membershipCreatedAt, + }}, + } + + actual, err := client.GetAddressMap(context.Background(), AccountIdentifier(testAccountID), "9a7806061c88ada191ed06f989cc3dac") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateAddressMap(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "9a7806061c88ada191ed06f989cc3dac", + "description": "My Ecommerce zones", + "can_delete": true, + "can_modify_ips": true, + "default_sni": "*.example.com", + "created_at": "2023-01-01T05:20:00.12345Z", + "modified_at": "2023-01-05T05:20:00.12345Z", + "enabled": true, + "ips": [ + { + "ip": "192.0.2.1", + "created_at": "2023-01-02T05:20:00.12345Z" + } + ], + "memberships": [ + { + "kind": "zone", + "identifier": "01a7362d577a6c3019a474fd6f485823", + "can_delete": true, + "created_at": "2023-01-03T05:20:00.12345Z" + } + ] + } + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac", handler) + + createdAt, _ := time.Parse(time.RFC3339, "2023-01-01T05:20:00.12345Z") + modifiedAt, _ := time.Parse(time.RFC3339, "2023-01-05T05:20:00.12345Z") + ipCreatedAt, _ := time.Parse(time.RFC3339, "2023-01-02T05:20:00.12345Z") + membershipCreatedAt, _ := time.Parse(time.RFC3339, "2023-01-03T05:20:00.12345Z") + + want := AddressMap{ + ID: "9a7806061c88ada191ed06f989cc3dac", + CreatedAt: createdAt, + ModifiedAt: modifiedAt, + Description: &addressMapDesc, + Deletable: BoolPtr(true), + CanModifyIPs: BoolPtr(true), + DefaultSNI: &addressMapDefaultSNI, + Enabled: BoolPtr(true), + IPs: []AddressMapIP{{"192.0.2.1", ipCreatedAt}}, + Memberships: []AddressMapMembership{{ + Identifier: "01a7362d577a6c3019a474fd6f485823", + Kind: AddressMapMembershipZone, + Deletable: BoolPtr(true), + CreatedAt: membershipCreatedAt, + }}, + } + + actual, err := client.UpdateAddressMap(context.Background(), AccountIdentifier(testAccountID), UpdateAddressMapParams{ID: "9a7806061c88ada191ed06f989cc3dac"}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteAddressMap(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "9a7806061c88ada191ed06f989cc3dac" + } + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac", handler) + + err := client.DeleteAddressMap(context.Background(), AccountIdentifier(testAccountID), "9a7806061c88ada191ed06f989cc3dac") + assert.NoError(t, err) +} + +func TestAddIPAddressToAddressMap(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) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac/ips/192.0.2.1", handler) + + err := client.CreateIPAddressToAddressMap(context.Background(), AccountIdentifier(testAccountID), CreateIPAddressToAddressMapParams{"9a7806061c88ada191ed06f989cc3dac", "192.0.2.1"}) + assert.NoError(t, err) +} + +func TestRemoveIPAddressFromAddressMap(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac/ips/192.0.2.1", handler) + + err := client.DeleteIPAddressFromAddressMap(context.Background(), AccountIdentifier(testAccountID), DeleteIPAddressFromAddressMapParams{"9a7806061c88ada191ed06f989cc3dac", "192.0.2.1"}) + assert.NoError(t, err) +} + +func TestAddZoneToAddressMap(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) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac/zones/01a7362d577a6c3019a474fd6f485823", handler) + + err := client.CreateMembershipToAddressMap(context.Background(), AccountIdentifier(testAccountID), CreateMembershipToAddressMapParams{"9a7806061c88ada191ed06f989cc3dac", AddressMapMembershipContainer{"01a7362d577a6c3019a474fd6f485823", AddressMapMembershipZone}}) + assert.NoError(t, err) +} + +func TestRemoveZoneFromAddressMap(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac/zones/01a7362d577a6c3019a474fd6f485823", handler) + + err := client.DeleteMembershipFromAddressMap(context.Background(), AccountIdentifier(testAccountID), DeleteMembershipFromAddressMapParams{"9a7806061c88ada191ed06f989cc3dac", AddressMapMembershipContainer{"01a7362d577a6c3019a474fd6f485823", AddressMapMembershipZone}}) + assert.NoError(t, err) +} + +func TestAddAccountToAddressMap(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) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac/accounts/01a7362d577a6c3019a474fd6f485823", handler) + + err := client.CreateMembershipToAddressMap(context.Background(), AccountIdentifier(testAccountID), CreateMembershipToAddressMapParams{"9a7806061c88ada191ed06f989cc3dac", AddressMapMembershipContainer{"01a7362d577a6c3019a474fd6f485823", AddressMapMembershipAccount}}) + assert.NoError(t, err) +} + +func TestRemoveAccountFromAddressMap(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/address_maps/9a7806061c88ada191ed06f989cc3dac/accounts/01a7362d577a6c3019a474fd6f485823", handler) + + err := client.DeleteMembershipFromAddressMap(context.Background(), AccountIdentifier(testAccountID), DeleteMembershipFromAddressMapParams{"9a7806061c88ada191ed06f989cc3dac", AddressMapMembershipContainer{"01a7362d577a6c3019a474fd6f485823", AddressMapMembershipAccount}}) + assert.NoError(t, err) +} diff --git a/ip_address_management.go b/addressing_ip_prefix.go similarity index 100% rename from ip_address_management.go rename to addressing_ip_prefix.go diff --git a/ip_address_management_test.go b/addressing_ip_prefix_test.go similarity index 100% rename from ip_address_management_test.go rename to addressing_ip_prefix_test.go diff --git a/cache_reserve.go b/cache_reserve.go new file mode 100644 index 0000000000..a62a6144ab --- /dev/null +++ b/cache_reserve.go @@ -0,0 +1,82 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// CacheReserve is the structure of the API object for the cache reserve +// setting. +type CacheReserve struct { + ID string `json:"id,omitempty"` + ModifiedOn time.Time `json:"modified_on,omitempty"` + Value string `json:"value"` +} + +// CacheReserveDetailsResponse is the API response for the cache reserve +// setting. +type CacheReserveDetailsResponse struct { + Result CacheReserve `json:"result"` + Response +} + +type zoneCacheReserveSingleResponse struct { + Response + Result CacheReserve `json:"result"` +} + +type GetCacheReserveParams struct{} + +type UpdateCacheReserveParams struct { + Value string `json:"value"` +} + +// GetCacheReserve returns information about the current cache reserve settings. +// +// API reference: https://developers.cloudflare.com/api/operations/zone-cache-settings-get-cache-reserve-setting +func (api *API) GetCacheReserve(ctx context.Context, rc *ResourceContainer, params GetCacheReserveParams) (CacheReserve, error) { + if rc.Level != ZoneRouteLevel { + return CacheReserve{}, ErrRequiredZoneLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/cache/cache_reserve", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return CacheReserve{}, err + } + + var cacheReserveDetailsResponse CacheReserveDetailsResponse + err = json.Unmarshal(res, &cacheReserveDetailsResponse) + if err != nil { + return CacheReserve{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return cacheReserveDetailsResponse.Result, nil +} + +// UpdateCacheReserve updates the cache reserve setting for a zone +// +// API reference: https://developers.cloudflare.com/api/operations/zone-cache-settings-change-cache-reserve-setting +func (api *API) UpdateCacheReserve(ctx context.Context, rc *ResourceContainer, params UpdateCacheReserveParams) (CacheReserve, error) { + if rc.Level != ZoneRouteLevel { + return CacheReserve{}, ErrRequiredZoneLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/cache/cache_reserve", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params) + if err != nil { + return CacheReserve{}, err + } + + response := &zoneCacheReserveSingleResponse{} + err = json.Unmarshal(res, &response) + if err != nil { + return CacheReserve{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return response.Result, nil +} diff --git a/cache_reserve_test.go b/cache_reserve_test.go new file mode 100644 index 0000000000..4434c9cf28 --- /dev/null +++ b/cache_reserve_test.go @@ -0,0 +1,82 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var cacheReserveTimestampString = "2019-02-20T22:37:07.107449Z" +var cacheReserveTimestamp, _ = time.Parse(time.RFC3339Nano, cacheReserveTimestampString) + +func TestCacheReserve(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "cache_reserve", + "value": "on", + "modified_on": "%s" + } + } + `, cacheReserveTimestampString) + } + + mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/cache/cache_reserve", handler) + want := CacheReserve{ + ID: "cache_reserve", + Value: "on", + ModifiedOn: cacheReserveTimestamp, + } + + actual, err := client.GetCacheReserve(context.Background(), ZoneIdentifier("01a7362d577a6c3019a474fd6f485823"), GetCacheReserveParams{}) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateCacheReserve(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "cache_reserve", + "value": "off", + "modified_on": "%s" + } + } + `, cacheReserveTimestampString) + } + + mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/cache/cache_reserve", handler) + want := CacheReserve{ + ID: "cache_reserve", + Value: "off", + ModifiedOn: cacheReserveTimestamp, + } + + actual, err := client.UpdateCacheReserve(context.Background(), ZoneIdentifier("01a7362d577a6c3019a474fd6f485823"), UpdateCacheReserveParams{Value: "off"}) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} diff --git a/catalog.json b/catalog.json new file mode 100644 index 0000000000..97847246e4 --- /dev/null +++ b/catalog.json @@ -0,0 +1,67 @@ +{ + "nixpkgs-flox": { + "x86_64-darwin": { + "stable": { + "go_1_20": { + "latest": { + "cache": { + "out": { + "https://cache.nixos.org": {} + } + }, + "element": { + "attrPath": [ + "legacyPackages", + "x86_64-darwin", + "go_1_20" + ], + "originalUrl": "flake:nixpkgs", + "storePaths": [ + "/nix/store/0pp0svlrkdls28dixb6a7kqa567gs59v-go-1.20.1" + ], + "url": "github:flox/nixpkgs/d0d55259081f0b97c828f38559cad899d351cad1" + }, + "eval": { + "meta": { + "description": "The Go Programming language", + "outputsToInstall": [ + "out" + ], + "position": "/nix/store/32cp7y0cf36h3xp0fqwy2nf432fpcaci-source/pkgs/development/compilers/go/1.20.nix:176", + "unfree": false + }, + "name": "go-1.20.1", + "outputs": { + "out": "/nix/store/0pp0svlrkdls28dixb6a7kqa567gs59v-go-1.20.1" + }, + "pname": "go", + "system": "x86_64-darwin", + "version": "1.20.1" + }, + "publish_element": { + "attrPath": [ + "evalCatalog", + "x86_64-darwin", + "stable", + "go_1_20" + ], + "originalUrl": "flake:nixpkgs-flox", + "storePaths": [ + "/nix/store/0pp0svlrkdls28dixb6a7kqa567gs59v-go-1.20.1" + ], + "url": "flake:nixpkgs-flox/b7e7e40e2aa1ca2a44db440f5dc52213564af02f" + }, + "source": { + "locked": { + "lastModified": 1676973346, + "revCount": 456418 + } + }, + "type": "catalogRender", + "version": 1 + } + } + } + } + } +} diff --git a/certificate_packs.go b/certificate_packs.go index cf499de936..802ed5dfe3 100644 --- a/certificate_packs.go +++ b/certificate_packs.go @@ -38,6 +38,7 @@ type CertificatePack struct { Hosts []string `json:"hosts"` Certificates []CertificatePackCertificate `json:"certificates"` PrimaryCertificate string `json:"primary_certificate"` + Status string `json:"status"` ValidationRecords []SSLValidationRecord `json:"validation_records,omitempty"` ValidationErrors []SSLValidationError `json:"validation_errors,omitempty"` ValidationMethod string `json:"validation_method"` diff --git a/certificate_packs_test.go b/certificate_packs_test.go index 07aece7c01..6dc9758247 100644 --- a/certificate_packs_test.go +++ b/certificate_packs_test.go @@ -42,6 +42,7 @@ var ( ID: "3822ff90-ea29-44df-9e55-21300bb9419b", Type: "advanced", Hosts: []string{"example.com", "*.example.com", "www.example.com"}, + Status: "initializing", ValidationMethod: "txt", ValidityDays: 90, CertificateAuthority: "lets_encrypt", @@ -244,6 +245,7 @@ func TestRestartAdvancedCertificateValidation(t *testing.T) { ID: "3822ff90-ea29-44df-9e55-21300bb9419b", Type: "advanced", Hosts: []string{"example.com", "*.example.com", "www.example.com"}, + Status: "initializing", ValidityDays: 365, ValidationMethod: "txt", CertificateAuthority: "lets_encrypt", diff --git a/cloudflare.go b/cloudflare.go index 952382864b..8e5afe369e 100644 --- a/cloudflare.go +++ b/cloudflare.go @@ -11,7 +11,9 @@ import ( "log" "math" "net/http" + "net/http/httputil" "net/url" + "regexp" "strconv" "strings" "time" @@ -65,8 +67,8 @@ func newClient(opts ...Option) (*API, error) { rateLimiter: rate.NewLimiter(rate.Limit(4), 1), // 4rps equates to default api limit (1200 req/5 min) retryPolicy: RetryPolicy{ MaxRetries: 3, - MinRetryDelay: time.Duration(1) * time.Second, - MaxRetryDelay: time.Duration(30) * time.Second, + MinRetryDelay: 1 * time.Second, + MaxRetryDelay: 30 * time.Second, }, logger: silentLogger, } @@ -250,20 +252,6 @@ func (api *API) makeRequestWithAuthTypeAndHeadersComplete(ctx context.Context, m return nil, fmt.Errorf("error caused by request rate limiting: %w", err) } - if api.Debug { - if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch { - buf := &bytes.Buffer{} - tee := io.TeeReader(reqBody, buf) - debugBody, _ := io.ReadAll(tee) - payloadBody, _ := io.ReadAll(buf) - fmt.Printf("cloudflare-go [DEBUG] REQUEST Method:%v URI:%s Headers:%#v Body:%v\n", method, api.BaseURL+uri, headers, string(debugBody)) - // ensure we recreate the io.Reader for use - reqBody = bytes.NewReader(payloadBody) - } else { - fmt.Printf("cloudflare-go [DEBUG] REQUEST Method:%v URI:%s Headers:%#v Body:%v\n", method, api.BaseURL+uri, headers, nil) - } - } - resp, respErr = api.request(ctx, method, uri, reqBody, authType, headers) // short circuit processing on context timeouts @@ -278,18 +266,8 @@ func (api *API) makeRequestWithAuthTypeAndHeadersComplete(ctx context.Context, m respErr = errors.New("exceeded available rate limit retries") } - // if we got a valid http response, try to read body so we can reuse the connection - // see https://golang.org/pkg/net/http/#Client.Do if respErr == nil { - respBody, err = io.ReadAll(resp.Body) - resp.Body.Close() - - respErr = fmt.Errorf("could not read response body: %w", err) - - api.logger.Printf("Request: %s %s got an error response %d: %s\n", method, uri, resp.StatusCode, - strings.Replace(strings.Replace(string(respBody), "\n", "", -1), "\t", "", -1)) - } else { - api.logger.Printf("Error performing request: %s %s : %s \n", method, uri, respErr.Error()) + respErr = fmt.Errorf("received %s response (HTTP %d), please try again later", strings.ToLower(http.StatusText(resp.StatusCode)), resp.StatusCode) } continue } else { @@ -298,6 +276,7 @@ func (api *API) makeRequestWithAuthTypeAndHeadersComplete(ctx context.Context, m if err != nil { return nil, fmt.Errorf("could not read response body: %w", err) } + break } } @@ -307,10 +286,6 @@ func (api *API) makeRequestWithAuthTypeAndHeadersComplete(ctx context.Context, m return nil, respErr } - if api.Debug { - fmt.Printf("cloudflare-go [DEBUG] RESPONSE StatusCode:%d RayID:%s ContentType:%s Body:%#v\n", resp.StatusCode, resp.Header.Get("cf-ray"), resp.Header.Get("content-type"), string(respBody)) - } - if resp.StatusCode >= http.StatusBadRequest { if strings.HasSuffix(resp.Request.URL.Path, "/filters/validate-expr") { return nil, fmt.Errorf("%s", respBody) @@ -408,11 +383,36 @@ func (api *API) request(ctx context.Context, method, uri string, reqBody io.Read req.Header.Set("Content-Type", "application/json") } + if api.Debug { + dump, err := httputil.DumpRequestOut(req, true) + if err != nil { + return nil, err + } + + // Strip out any sensitive information from the request payload. + sensitiveKeys := []string{api.APIKey, api.APIEmail, api.APIToken, api.APIUserServiceKey} + for _, key := range sensitiveKeys { + if key != "" { + valueRegex := regexp.MustCompile(fmt.Sprintf("(?m)%s", key)) + dump = valueRegex.ReplaceAll(dump, []byte("[redacted]")) + } + } + log.Printf("\n%s", string(dump)) + } + resp, err := api.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } + if api.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s", string(dump)) + } + return resp, nil } @@ -596,3 +596,10 @@ func checkResultInfo(perPage, page, count int, info *ResultInfo) bool { panic("checkResultInfo: impossible") } } + +type OrderDirection string + +const ( + OrderDirectionAsc OrderDirection = "asc" + OrderDirectionDesc OrderDirection = "desc" +) diff --git a/cloudflare_experimental.go b/cloudflare_experimental.go index 009d3e1249..c4bc4934ea 100644 --- a/cloudflare_experimental.go +++ b/cloudflare_experimental.go @@ -6,8 +6,11 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" + "net/http/httputil" "net/url" + "regexp" "strings" "sync" "time" @@ -98,13 +101,13 @@ func NewExperimental(config *ClientParams) (*Client, error) { if c.RetryPolicy.MinRetryDelay > 0 { retryClient.RetryWaitMin = c.RetryPolicy.MinRetryDelay } else { - retryClient.RetryWaitMin = time.Duration(1) * time.Second + retryClient.RetryWaitMin = 1 * time.Second } if c.RetryPolicy.MaxRetryDelay > 0 { retryClient.RetryWaitMax = c.RetryPolicy.MaxRetryDelay } else { - retryClient.RetryWaitMax = time.Duration(30) * time.Second + retryClient.RetryWaitMax = 30 * time.Second } retryClient.Logger = silentRetryLogger @@ -193,11 +196,36 @@ func (c *Client) request(ctx context.Context, method, uri string, reqBody io.Rea req.Header.Set("Content-Type", "application/json") } + if c.Debug { + dump, err := httputil.DumpRequestOut(req, true) + if err != nil { + return nil, err + } + + // Strip out any sensitive information from the request payload. + sensitiveKeys := []string{c.Key, c.Email, c.Token, c.UserServiceKey} + for _, key := range sensitiveKeys { + if key != "" { + valueRegex := regexp.MustCompile(fmt.Sprintf("(?m)%s", key)) + dump = valueRegex.ReplaceAll(dump, []byte("[redacted]")) + } + } + log.Printf("\n%s", string(dump)) + } + resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } + if c.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s", string(dump)) + } + return resp, nil } @@ -224,25 +252,11 @@ func (c *Client) makeRequest(ctx context.Context, method, uri string, params int var respErr error var respBody []byte - if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch { - buf := &bytes.Buffer{} - tee := io.TeeReader(reqBody, buf) - debugBody, _ := io.ReadAll(tee) - payloadBody, _ := io.ReadAll(buf) - c.Logger.Debugf("REQUEST Method:%v URI:%s Headers:%#v Body:%v\n", method, c.BaseURL.String()+uri, headers, string(debugBody)) - // ensure we recreate the io.Reader for use - reqBody = bytes.NewReader(payloadBody) - } else { - c.Logger.Debugf("REQUEST Method:%v URI:%s Headers:%#v Body:%v\n", method, c.BaseURL.String()+uri, headers, nil) //) - } - resp, respErr = c.request(ctx, method, uri, reqBody, headers) if respErr != nil { return nil, respErr } - c.Logger.Debugf("RESPONSE URI:%s StatusCode:%d Body:%#v RayID:%s\n", c.BaseURL.String()+uri, resp.StatusCode, string(respBody), resp.Header.Get("cf-ray")) - respBody, err = io.ReadAll(resp.Body) resp.Body.Close() if err != nil { diff --git a/cloudflare_test.go b/cloudflare_test.go index 24ec3154f2..831b1464d9 100644 --- a/cloudflare_test.go +++ b/cloudflare_test.go @@ -495,18 +495,18 @@ func TestContextTimeout(t *testing.T) { defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { - time.Sleep(time.Second * time.Duration(3)) + time.Sleep(3 * time.Second) } mux.HandleFunc("/timeout", handler) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(1)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() start := time.Now() _, err := client.makeRequestContext(ctx, http.MethodHead, "/timeout", nil) assert.ErrorIs(t, err, context.DeadlineExceeded) - assert.WithinDuration(t, start, time.Now(), time.Second*2, + assert.WithinDuration(t, start, time.Now(), 2*time.Second, "makeRequestContext took too much time with an expiring context") } diff --git a/cmd/flarectl/dns.go b/cmd/flarectl/dns.go index db9653e037..5199e10271 100644 --- a/cmd/flarectl/dns.go +++ b/cmd/flarectl/dns.go @@ -42,7 +42,7 @@ func dnsCreate(c *cli.Context) error { return err } - record := cloudflare.DNSRecord{ + record := cloudflare.CreateDNSRecordParams{ Name: name, Type: strings.ToUpper(rtype), Content: content, @@ -50,14 +50,14 @@ func dnsCreate(c *cli.Context) error { Proxied: &proxy, Priority: &priority, } - resp, err := api.CreateDNSRecord(context.Background(), zoneID, record) + result, err := api.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), record) if err != nil { fmt.Fprintln(os.Stderr, "Error creating DNS record: ", err) return err } output := [][]string{ - formatDNSRecord(resp.Result), + formatDNSRecord(result), } writeTable(c, output, "ID", "Name", "Type", "Content", "TTL", "Proxiable", "Proxy", "Locked") @@ -84,23 +84,20 @@ func dnsCreateOrUpdate(c *cli.Context) error { return err } - // Look for an existing record - rr := cloudflare.DNSRecord{ - Name: name + "." + zone, - } - records, err := api.DNSRecords(context.Background(), zoneID, rr) + records, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Name: name + "." + zone}) if err != nil { fmt.Fprintln(os.Stderr, "Error fetching DNS records: ", err) return err } - var resp *cloudflare.DNSRecordResponse + var result cloudflare.DNSRecord if len(records) > 0 { // Record exists - find the ID and update it. // This is imprecise without knowing the original content; if a label // has multiple RRs we'll just update the first one. for _, r := range records { if r.Type == rtype { + rr := cloudflare.UpdateDNSRecordParams{} rr.ID = r.ID rr.Type = r.Type rr.Content = content @@ -108,25 +105,23 @@ func dnsCreateOrUpdate(c *cli.Context) error { rr.Proxied = &proxy rr.Priority = &priority - err := api.UpdateDNSRecord(context.Background(), zoneID, r.ID, rr) + result, err = api.UpdateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), rr) if err != nil { fmt.Println("Error updating DNS record:", err) return err } - resp = &cloudflare.DNSRecordResponse{ - Result: rr, - } } } } else { // Record doesn't exist - create it + rr := cloudflare.CreateDNSRecordParams{} rr.Type = rtype rr.Content = content rr.TTL = ttl rr.Proxied = &proxy rr.Priority = &priority // TODO: Print the response. - resp, err = api.CreateDNSRecord(context.Background(), zoneID, rr) + result, err = api.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), rr) if err != nil { fmt.Println("Error creating DNS record:", err) return err @@ -134,7 +129,7 @@ func dnsCreateOrUpdate(c *cli.Context) error { } output := [][]string{ - formatDNSRecord(resp.Result), + formatDNSRecord(result), } writeTable(c, output, "ID", "Name", "Type", "Content", "TTL", "Proxiable", "Proxy", "Locked") @@ -162,7 +157,7 @@ func dnsUpdate(c *cli.Context) error { return err } - record := cloudflare.DNSRecord{ + record := cloudflare.UpdateDNSRecordParams{ ID: recordID, Name: name, Type: strings.ToUpper(rtype), @@ -171,7 +166,7 @@ func dnsUpdate(c *cli.Context) error { Proxied: &proxy, Priority: &priority, } - err = api.UpdateDNSRecord(context.Background(), zoneID, recordID, record) + _, err = api.UpdateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), record) if err != nil { fmt.Fprintln(os.Stderr, "Error updating DNS record: ", err) return err @@ -194,7 +189,7 @@ func dnsDelete(c *cli.Context) error { return err } - err = api.DeleteDNSRecord(context.Background(), zoneID, recordID) + err = api.DeleteDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), recordID) if err != nil { fmt.Fprintln(os.Stderr, "Error deleting DNS record: ", err) return err diff --git a/cmd/flarectl/misc.go b/cmd/flarectl/misc.go index edad3d9803..ddfeeb6cfb 100644 --- a/cmd/flarectl/misc.go +++ b/cmd/flarectl/misc.go @@ -203,7 +203,7 @@ func pageRules(c *cli.Context) error { } func originCARootCertificate(c *cli.Context) error { - cert, err := cloudflare.OriginCARootCertificate(c.String("algorithm")) + cert, err := cloudflare.GetOriginCARootCertificate(c.String("algorithm")) if err != nil { return err } diff --git a/cmd/flarectl/zone.go b/cmd/flarectl/zone.go index 5cd2c81a4c..5ab4e24b43 100644 --- a/cmd/flarectl/zone.go +++ b/cmd/flarectl/zone.go @@ -279,11 +279,11 @@ func zoneRecords(c *cli.Context) error { return err } - // Create a an empty record for searching for records - rr := cloudflare.DNSRecord{} + // Create an empty record for searching for records + rr := cloudflare.ListDNSRecordsParams{} var records []cloudflare.DNSRecord if c.String("id") != "" { - rec, err := api.DNSRecord(context.Background(), zoneID, c.String("id")) + rec, err := api.GetDNSRecord(context.Background(), cloudflare.ZoneIdentifier(zoneID), c.String("id")) if err != nil { fmt.Println(err) return err @@ -300,7 +300,7 @@ func zoneRecords(c *cli.Context) error { rr.Content = c.String("content") } var err error - records, err = api.DNSRecords(context.Background(), zoneID, rr) + records, _, err = api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), rr) if err != nil { fmt.Println(err) return err @@ -310,7 +310,7 @@ func zoneRecords(c *cli.Context) error { for _, r := range records { switch r.Type { case "MX": - r.Content = fmt.Sprintf("%d %s", r.Priority, r.Content) + r.Content = fmt.Sprintf("%d %s", *r.Priority, r.Content) case "SRV": dp := r.Data.(map[string]interface{}) r.Content = fmt.Sprintf("%.f %s", dp["priority"], r.Content) diff --git a/convert_types.go b/convert_types.go index 1d45d34557..f3ebc83d0d 100644 --- a/convert_types.go +++ b/convert_types.go @@ -24,24 +24,24 @@ import ( // // Usage: var _ *Type = AnyPtr(Type(value) | value).(*Type) // -// var _ *bool = AnyPtr(true).(*bool) -// var _ *byte = AnyPtr(byte(1)).(*byte) -// var _ *complex64 = AnyPtr(complex64(1.1)).(*complex64) -// var _ *complex128 = AnyPtr(complex128(1.1)).(*complex128) -// var _ *float32 = AnyPtr(float32(1.1)).(*float32) -// var _ *float64 = AnyPtr(float64(1.1)).(*float64) -// var _ *int = AnyPtr(int(1)).(*int) -// var _ *int8 = AnyPtr(int8(8)).(*int8) -// var _ *int16 = AnyPtr(int16(16)).(*int16) -// var _ *int32 = AnyPtr(int32(32)).(*int32) -// var _ *int64 = AnyPtr(int64(64)).(*int64) -// var _ *rune = AnyPtr(rune(1)).(*rune) -// var _ *string = AnyPtr("ptr").(*string) -// var _ *uint = AnyPtr(uint(1)).(*uint) -// var _ *uint8 = AnyPtr(uint8(8)).(*uint8) -// var _ *uint16 = AnyPtr(uint16(16)).(*uint16) -// var _ *uint32 = AnyPtr(uint32(32)).(*uint32) -// var _ *uint64 = AnyPtr(uint64(64)).(*uint64) +// var _ *bool = AnyPtr(true).(*bool) +// var _ *byte = AnyPtr(byte(1)).(*byte) +// var _ *complex64 = AnyPtr(complex64(1.1)).(*complex64) +// var _ *complex128 = AnyPtr(complex128(1.1)).(*complex128) +// var _ *float32 = AnyPtr(float32(1.1)).(*float32) +// var _ *float64 = AnyPtr(float64(1.1)).(*float64) +// var _ *int = AnyPtr(int(1)).(*int) +// var _ *int8 = AnyPtr(int8(8)).(*int8) +// var _ *int16 = AnyPtr(int16(16)).(*int16) +// var _ *int32 = AnyPtr(int32(32)).(*int32) +// var _ *int64 = AnyPtr(int64(64)).(*int64) +// var _ *rune = AnyPtr(rune(1)).(*rune) +// var _ *string = AnyPtr("ptr").(*string) +// var _ *uint = AnyPtr(uint(1)).(*uint) +// var _ *uint8 = AnyPtr(uint8(8)).(*uint8) +// var _ *uint16 = AnyPtr(uint16(16)).(*uint16) +// var _ *uint32 = AnyPtr(uint32(32)).(*uint32) +// var _ *uint64 = AnyPtr(uint64(64)).(*uint64) func AnyPtr(v interface{}) interface{} { r := reflect.New(reflect.TypeOf(v)) reflect.ValueOf(r.Interface()).Elem().Set(reflect.ValueOf(v)) diff --git a/device_posture_rule.go b/device_posture_rule.go index 288d5a2e53..5d04e270b5 100644 --- a/device_posture_rule.go +++ b/device_posture_rule.go @@ -162,25 +162,30 @@ type DevicePostureRuleMatch struct { // DevicePostureRuleInput represents the value to be checked against. type DevicePostureRuleInput struct { - ID string `json:"id,omitempty"` - Path string `json:"path,omitempty"` - Exists bool `json:"exists,omitempty"` - Thumbprint string `json:"thumbprint,omitempty"` - Sha256 string `json:"sha256,omitempty"` - Running bool `json:"running,omitempty"` - RequireAll bool `json:"requireAll,omitempty"` - Enabled bool `json:"enabled,omitempty"` - Version string `json:"version,omitempty"` - VersionOperator string `json:"versionOperator,omitempty"` - Overall string `json:"overall,omitempty"` - SensorConfig string `json:"sensor_config,omitempty"` - Os string `json:"os,omitempty"` - OsDistroName string `json:"os_distro_name,omitempty"` - OsDistroRevision string `json:"os_distro_revision,omitempty"` - Operator string `json:"operator,omitempty"` - Domain string `json:"domain,omitempty"` - ComplianceStatus string `json:"compliance_status,omitempty"` - ConnectionID string `json:"connection_id,omitempty"` + ID string `json:"id,omitempty"` + Path string `json:"path,omitempty"` + Exists bool `json:"exists,omitempty"` + Thumbprint string `json:"thumbprint,omitempty"` + Sha256 string `json:"sha256,omitempty"` + Running bool `json:"running,omitempty"` + RequireAll bool `json:"requireAll,omitempty"` + CheckDisks []string `json:"checkDisks,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Version string `json:"version,omitempty"` + VersionOperator string `json:"versionOperator,omitempty"` + Overall string `json:"overall,omitempty"` + SensorConfig string `json:"sensor_config,omitempty"` + Os string `json:"os,omitempty"` + OsDistroName string `json:"os_distro_name,omitempty"` + OsDistroRevision string `json:"os_distro_revision,omitempty"` + Operator string `json:"operator,omitempty"` + Domain string `json:"domain,omitempty"` + ComplianceStatus string `json:"compliance_status,omitempty"` + ConnectionID string `json:"connection_id,omitempty"` + IssueCount string `json:"issue_count,omitempty"` + CountOperator string `json:"countOperator,omitempty"` + TotalScore string `json:"total_score,omitempty"` + ScoreOperator string `json:"scoreOperator,omitempty"` } // DevicePostureRuleListResponse represents the response from the list diff --git a/device_posture_rule_test.go b/device_posture_rule_test.go index 03be127215..c7ba0f4689 100644 --- a/device_posture_rule_test.go +++ b/device_posture_rule_test.go @@ -402,7 +402,8 @@ func TestDevicePostureDiskEncryptionRule(t *testing.T) { } ], "input": { - "requireAll": true + "requireAll": true, + "checkDisks": ["C", "D"] } } } @@ -419,6 +420,7 @@ func TestDevicePostureDiskEncryptionRule(t *testing.T) { Match: []DevicePostureRuleMatch{{Platform: "ios"}}, Input: DevicePostureRuleInput{ RequireAll: true, + CheckDisks: []string{"C", "D"}, }, } diff --git a/devices_dex.go b/devices_dex.go new file mode 100644 index 0000000000..f95cb9b5cf --- /dev/null +++ b/devices_dex.go @@ -0,0 +1,173 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type DeviceDexTestData map[string]interface{} + +type DeviceDexTest struct { + TestID string `json:"test_id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Interval string `json:"interval"` + Enabled bool `json:"enabled"` + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + Data *DeviceDexTestData `json:"data"` +} + +type DeviceDexTests struct { + DexTests []DeviceDexTest `json:"dex_tests"` +} + +type DeviceDexTestResponse struct { + Response + Result DeviceDexTest `json:"result"` +} + +type DeviceDexTestListResponse struct { + Response + Result DeviceDexTests `json:"result"` +} + +type ListDeviceDexTestParams struct{} + +type CreateDeviceDexTestParams struct { + TestID string `json:"test_id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Interval string `json:"interval"` + Enabled bool `json:"enabled"` + Data *DeviceDexTestData `json:"data"` +} + +type UpdateDeviceDexTestParams struct { + TestID string `json:"test_id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Interval string `json:"interval"` + Enabled bool `json:"enabled"` + Data *DeviceDexTestData `json:"data"` +} + +// ListDexTests returns all Device Dex Tests for a given account. +// +// API reference : https://developers.cloudflare.com/api/operations/device-dex-test-details +func (api *API) ListDexTests(ctx context.Context, rc *ResourceContainer, params ListDeviceDexTestParams) (DeviceDexTests, error) { + if rc.Level != AccountRouteLevel { + return DeviceDexTests{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/dex_tests", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return DeviceDexTests{}, err + } + + var response DeviceDexTestListResponse + err = json.Unmarshal(res, &response) + if err != nil { + return DeviceDexTests{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return response.Result, nil +} + +// CreateDeviceDexTest created a new Device Dex Test +// +// API reference: https://developers.cloudflare.com/api/operations/device-dex-test-create-device-dex-test +func (api *API) CreateDeviceDexTest(ctx context.Context, rc *ResourceContainer, params CreateDeviceDexTestParams) (DeviceDexTest, error) { + if rc.Level != AccountRouteLevel { + return DeviceDexTest{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/dex_tests", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return DeviceDexTest{}, err + } + + var deviceDexTestResponse DeviceDexTestResponse + if err := json.Unmarshal(res, &deviceDexTestResponse); err != nil { + return DeviceDexTest{}, fmt.Errorf("%s: %w\n\nres: %s", errUnmarshalError, err, string(res)) + } + + return deviceDexTestResponse.Result, err +} + +// UpdateDeviceDexTest Updates a Device Dex Test. +// +// API reference: https://developers.cloudflare.com/api/operations/device-dex-test-update-device-dex-test +func (api *API) UpdateDeviceDexTest(ctx context.Context, rc *ResourceContainer, params UpdateDeviceDexTestParams) (DeviceDexTest, error) { + if rc.Level != AccountRouteLevel { + return DeviceDexTest{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/dex_tests/%s", rc.Level, rc.Identifier, params.TestID) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return DeviceDexTest{}, err + } + + var deviceDexTestsResponse DeviceDexTestResponse + + if err := json.Unmarshal(res, &deviceDexTestsResponse); err != nil { + return DeviceDexTest{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return deviceDexTestsResponse.Result, err +} + +// GetDeviceDexTest gets a single Device Dex Test. +// +// API reference: https://developers.cloudflare.com/api/operations/device-dex-test-get-device-dex-test +func (api *API) GetDeviceDexTest(ctx context.Context, rc *ResourceContainer, testID string) (DeviceDexTest, error) { + if rc.Level != AccountRouteLevel { + return DeviceDexTest{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/dex_tests/%s", rc.Level, rc.Identifier, testID) + + deviceDexTestResponse := DeviceDexTestResponse{} + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return DeviceDexTest{}, err + } + + if err := json.Unmarshal(res, &deviceDexTestResponse); err != nil { + return DeviceDexTest{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return deviceDexTestResponse.Result, err +} + +// DeleteDexTest deletes a Device Dex Test. +// +// API reference: https://developers.cloudflare.com/api/operations/device-dex-test-delete-device-dex-test +func (api *API) DeleteDexTest(ctx context.Context, rc *ResourceContainer, testID string) (DeviceDexTests, error) { + if rc.Level != AccountRouteLevel { + return DeviceDexTests{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/dex_tests/%s", rc.Level, rc.Identifier, testID) + + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return DeviceDexTests{}, err + } + + var response DeviceDexTestListResponse + if err := json.Unmarshal(res, &response); err != nil { + return DeviceDexTests{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return response.Result, err +} diff --git a/devices_dex_test.go b/devices_dex_test.go new file mode 100644 index 0000000000..f5901f7916 --- /dev/null +++ b/devices_dex_test.go @@ -0,0 +1,283 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const testID = "f174e90a-fafe-4643-bbbc-4a0ed4fc8415" + +var dexTimestamp, _ = time.Parse(time.RFC3339, "2023-01-30T19:59:44.401278Z") + +func TestGetDeviceDexTests(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "dex_tests": [ + { + "test_id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "name": "http test dash", + "description": "dex test description", + "interval": "0h30m0s", + "enabled": true, + "data": { + "host": "https://dash.cloudflare.com", + "kind": "http", + "method": "GET" + }, + "updated": "2023-01-30T19:59:44.401278Z", + "created": "2023-01-30T19:59:44.401278Z" + } + ] + } + }`) + } + + dexTest := []DeviceDexTest{{ + TestID: testID, + Name: "http test dash", + Description: "dex test description", + Interval: "0h30m0s", + Enabled: true, + Data: &DeviceDexTestData{ + "kind": "http", + "method": "GET", + "host": "https://dash.cloudflare.com", + }, + Updated: dexTimestamp, + Created: dexTimestamp, + }} + + want := DeviceDexTests{ + DexTests: dexTest, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/dex_tests", handler) + + actual, err := client.ListDexTests(context.Background(), AccountIdentifier(testAccountID), ListDeviceDexTestParams{}) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeviceDexTest(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "test_id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "name": "http test dash", + "description": "dex test description", + "interval": "0h30m0s", + "enabled": true, + "data": { + "host": "https://dash.cloudflare.com", + "kind": "http", + "method": "GET" + }, + "updated": "2023-01-30T19:59:44.401278Z", + "created": "2023-01-30T19:59:44.401278Z" + } + }`) + } + + want := DeviceDexTest{ + TestID: testID, + Name: "http test dash", + Description: "dex test description", + Interval: "0h30m0s", + Enabled: true, + Data: &DeviceDexTestData{ + "kind": "http", + "method": "GET", + "host": "https://dash.cloudflare.com", + }, + Updated: dexTimestamp, + Created: dexTimestamp, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/dex_tests/"+testID, handler) + + actual, err := client.GetDeviceDexTest(context.Background(), AccountIdentifier(testAccountID), testID) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateDeviceDexTest(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "test_id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "name": "http test dash", + "description": "dex test description", + "interval": "0h30m0s", + "enabled": true, + "data": { + "host": "https://dash.cloudflare.com", + "kind": "http", + "method": "GET" + }, + "updated": "2023-01-30T19:59:44.401278Z", + "created": "2023-01-30T19:59:44.401278Z" + } + }`) + } + + want := DeviceDexTest{ + TestID: testID, + Name: "http test dash", + Description: "dex test description", + Interval: "0h30m0s", + Enabled: true, + Data: &DeviceDexTestData{ + "kind": "http", + "method": "GET", + "host": "https://dash.cloudflare.com", + }, + Updated: dexTimestamp, + Created: dexTimestamp, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/dex_tests", handler) + + actual, err := client.CreateDeviceDexTest(context.Background(), AccountIdentifier(testAccountID), CreateDeviceDexTestParams{ + TestID: testID, + Name: "http test dash", + Description: "dex test description", + Interval: "0h30m0s", + Enabled: true, + Data: &DeviceDexTestData{ + "kind": "http", + "method": "GET", + "host": "https://dash.cloudflare.com", + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateDeviceDexTest(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) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "test_id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "name": "http test dash", + "description": "dex test description", + "interval": "0h30m0s", + "enabled": true, + "data": { + "host": "https://dash.cloudflare.com", + "kind": "http", + "method": "GET" + }, + "updated": "2023-01-30T19:59:44.401278Z", + "created": "2023-01-30T19:59:44.401278Z" + } + }`) + } + + want := DeviceDexTest{ + TestID: testID, + Name: "http test dash", + Description: "dex test description", + Interval: "0h30m0s", + Enabled: true, + Data: &DeviceDexTestData{ + "kind": "http", + "method": "GET", + "host": "https://dash.cloudflare.com", + }, + Updated: dexTimestamp, + Created: dexTimestamp, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/dex_tests/"+testID, handler) + + actual, err := client.UpdateDeviceDexTest(context.Background(), AccountIdentifier(testAccountID), UpdateDeviceDexTestParams{ + TestID: testID, + Name: "http test dash", + Description: "dex test description", + Interval: "0h30m0s", + Enabled: true, + Data: &DeviceDexTestData{ + "kind": "http", + "method": "GET", + "host": "https://dash.cloudflare.com", + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteDeviceDexTest(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "dex_tests": [] + } + }`) + } + + want := DeviceDexTests{ + DexTests: []DeviceDexTest{}, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/dex_tests/"+testID, handler) + + actual, err := client.DeleteDexTest(context.Background(), AccountIdentifier(testAccountID), testID) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} diff --git a/devices_managed_networks.go b/devices_managed_networks.go new file mode 100644 index 0000000000..2e308eb213 --- /dev/null +++ b/devices_managed_networks.go @@ -0,0 +1,164 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +type Config struct { + TlsSockAddr string `json:"tls_sockaddr,omitempty"` + Sha256 string `json:"sha256,omitempty"` +} + +type DeviceManagedNetwork struct { + NetworkID string `json:"network_id,omitempty"` + Type string `json:"type"` + Name string `json:"name"` + Config *Config `json:"config"` +} + +type DeviceManagedNetworkResponse struct { + Response + Result DeviceManagedNetwork `json:"result"` +} + +type DeviceManagedNetworkListResponse struct { + Response + Result []DeviceManagedNetwork `json:"result"` +} + +type ListDeviceManagedNetworksParams struct{} + +type CreateDeviceManagedNetworkParams struct { + NetworkID string `json:"network_id,omitempty"` + Type string `json:"type"` + Name string `json:"name"` + Config *Config `json:"config"` +} + +type UpdateDeviceManagedNetworkParams struct { + NetworkID string `json:"network_id,omitempty"` + Type string `json:"type"` + Name string `json:"name"` + Config *Config `json:"config"` +} + +// ListDeviceManagedNetwork returns all Device Managed Networks for a given +// account. +// +// API reference : https://api.cloudflare.com/#device-managed-networks-list-device-managed-networks +func (api *API) ListDeviceManagedNetworks(ctx context.Context, rc *ResourceContainer, params ListDeviceManagedNetworksParams) ([]DeviceManagedNetwork, error) { + if rc.Level != AccountRouteLevel { + return []DeviceManagedNetwork{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/networks", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []DeviceManagedNetwork{}, err + } + + var response DeviceManagedNetworkListResponse + err = json.Unmarshal(res, &response) + if err != nil { + return []DeviceManagedNetwork{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return response.Result, nil +} + +// CreateDeviceManagedNetwork creates a new Device Managed Network. +// +// API reference: https://api.cloudflare.com/#device-managed-networks-create-device-managed-network +func (api *API) CreateDeviceManagedNetwork(ctx context.Context, rc *ResourceContainer, params CreateDeviceManagedNetworkParams) (DeviceManagedNetwork, error) { + if rc.Level != AccountRouteLevel { + return DeviceManagedNetwork{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/networks", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return DeviceManagedNetwork{}, err + } + + var deviceManagedNetworksResponse DeviceManagedNetworkResponse + if err := json.Unmarshal(res, &deviceManagedNetworksResponse); err != nil { + return DeviceManagedNetwork{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return deviceManagedNetworksResponse.Result, err +} + +// UpdateDeviceManagedNetwork Update a Device Managed Network. +// +// API reference: https://api.cloudflare.com/#device-managed-networks-update-device-managed-network +func (api *API) UpdateDeviceManagedNetwork(ctx context.Context, rc *ResourceContainer, params UpdateDeviceManagedNetworkParams) (DeviceManagedNetwork, error) { + if rc.Level != AccountRouteLevel { + return DeviceManagedNetwork{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/networks/%s", rc.Level, rc.Identifier, params.NetworkID) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return DeviceManagedNetwork{}, err + } + + var deviceManagedNetworksResponse DeviceManagedNetworkResponse + + if err := json.Unmarshal(res, &deviceManagedNetworksResponse); err != nil { + return DeviceManagedNetwork{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return deviceManagedNetworksResponse.Result, err +} + +// GetDeviceManagedNetwork gets a single Device Managed Network. +// +// API reference: https://api.cloudflare.com/#device-managed-networks-device-managed-network-details +func (api *API) GetDeviceManagedNetwork(ctx context.Context, rc *ResourceContainer, networkID string) (DeviceManagedNetwork, error) { + if rc.Level != AccountRouteLevel { + return DeviceManagedNetwork{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/networks/%s", rc.Level, rc.Identifier, networkID) + + deviceManagedNetworksResponse := DeviceManagedNetworkResponse{} + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return DeviceManagedNetwork{}, err + } + + if err := json.Unmarshal(res, &deviceManagedNetworksResponse); err != nil { + return DeviceManagedNetwork{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return deviceManagedNetworksResponse.Result, err +} + +// DeleteManagedNetworks deletes a Device Managed Network. +// +// API reference: https://api.cloudflare.com/#device-managed-networks-delete-device-managed-network +func (api *API) DeleteManagedNetworks(ctx context.Context, rc *ResourceContainer, networkID string) ([]DeviceManagedNetwork, error) { + if rc.Level != AccountRouteLevel { + return []DeviceManagedNetwork{}, ErrRequiredAccountLevelResourceContainer + } + + uri := fmt.Sprintf("/%s/%s/devices/networks/%s", rc.Level, rc.Identifier, networkID) + + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return []DeviceManagedNetwork{}, err + } + + var response DeviceManagedNetworkListResponse + if err := json.Unmarshal(res, &response); err != nil { + return []DeviceManagedNetwork{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return response.Result, err +} diff --git a/devices_managed_networks_test.go b/devices_managed_networks_test.go new file mode 100644 index 0000000000..c62c7c6792 --- /dev/null +++ b/devices_managed_networks_test.go @@ -0,0 +1,245 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testNetworkID = "f174e90a-fafe-4643-bbbc-4a0ed4fc8415" + +func TestGetDeviceManagedNetworks(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "network_id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "type": "tls", + "name": "managed-network-1", + "config": { + "tls_sockaddr": "foobar:1234", + "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" + } + } + ] + }`) + } + + want := []DeviceManagedNetwork{{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + }} + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/networks", handler) + + actual, err := client.ListDeviceManagedNetworks(context.Background(), AccountIdentifier(testAccountID), ListDeviceManagedNetworksParams{}) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeviceManagedNetwork(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": + { + "network_id": "%s", + "type": "tls", + "name": "managed-network-1", + "config": { + "tls_sockaddr": "foobar:1234", + "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" + } + } + }`, testNetworkID) + } + + want := DeviceManagedNetwork{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/networks/"+testNetworkID, handler) + + actual, err := client.GetDeviceManagedNetwork(context.Background(), AccountIdentifier(testAccountID), testNetworkID) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateDeviceManagedNetwork(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": + { + "network_id": "%s", + "type": "tls", + "name": "managed-network-1", + "config": { + "tls_sockaddr": "foobar:1234", + "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" + } + } + }`, testNetworkID) + } + + want := DeviceManagedNetwork{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/networks", handler) + + actual, err := client.CreateDeviceManagedNetwork(context.Background(), AccountIdentifier(testAccountID), CreateDeviceManagedNetworkParams{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateDeviceManagedNetwork(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) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": + { + "network_id": "%s", + "type": "tls", + "name": "managed-network-1", + "config": { + "tls_sockaddr": "foobar:1234", + "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" + } + } + }`, testNetworkID) + } + + want := DeviceManagedNetwork{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/networks/"+testNetworkID, handler) + + actual, err := client.UpdateDeviceManagedNetwork(context.Background(), AccountIdentifier(testAccountID), UpdateDeviceManagedNetworkParams{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteDeviceManagedNetwork(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "network_id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "type": "tls", + "name": "managed-network-1", + "config": { + "tls_sockaddr": "foobar:1234", + "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" + } + } + ] + }`) + } + + want := []DeviceManagedNetwork{{ + NetworkID: testNetworkID, + Type: "tls", + Name: "managed-network-1", + Config: &Config{ + TlsSockAddr: "foobar:1234", + Sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", + }, + }} + + mux.HandleFunc("/accounts/"+testAccountID+"/devices/networks/"+testNetworkID, handler) + + actual, err := client.DeleteManagedNetworks(context.Background(), AccountIdentifier(testAccountID), testNetworkID) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} diff --git a/devices_policy.go b/devices_policy.go index 96d542125e..2664a260c7 100644 --- a/devices_policy.go +++ b/devices_policy.go @@ -17,9 +17,19 @@ type DeviceClientCertificatesZone struct { Result Enabled } +type ServiceMode string + +const ( + oneDotOne ServiceMode = "1dot1" + warp ServiceMode = "warp" + proxy ServiceMode = "proxy" + postureOnly ServiceMode = "posture_only" + warpTunnelOnly ServiceMode = "warp_tunnel_only" +) + type ServiceModeV2 struct { - Mode string `json:"mode,omitempty"` - Port int `json:"port,omitempty"` + Mode ServiceMode `json:"mode,omitempty"` + Port int `json:"port,omitempty"` } type DeviceSettingsPolicy struct { @@ -42,6 +52,7 @@ type DeviceSettingsPolicy struct { Match *string `json:"match"` Precedence *int `json:"precedence"` Default bool `json:"default"` + ExcludeOfficeIps *bool `json:"exclude_office_ips"` } type DeviceSettingsPolicyResponse struct { @@ -68,6 +79,7 @@ type DeviceSettingsPolicyRequest struct { Name *string `json:"name,omitempty"` Match *string `json:"match,omitempty"` Enabled *bool `json:"enabled,omitempty"` + ExcludeOfficeIps *bool `json:"exclude_office_ips"` } // UpdateDeviceClientCertificates controls the zero trust zone used to provision client certificates. diff --git a/devices_policy_test.go b/devices_policy_test.go index 12bd073549..ac03a752c7 100644 --- a/devices_policy_test.go +++ b/devices_policy_test.go @@ -27,20 +27,21 @@ var ( {Address: "10.0.0.0/8"}, {Address: "100.64.0.0/10"}, }, - GatewayUniqueID: StringPtr("t1235"), - SupportURL: StringPtr(""), - CaptivePortal: IntPtr(180), - AllowModeSwitch: BoolPtr(false), - SwitchLocked: BoolPtr(false), - AllowUpdates: BoolPtr(false), - AutoConnect: IntPtr(0), - AllowedToLeave: BoolPtr(true), - Enabled: BoolPtr(true), - PolicyID: nil, - Name: nil, - Match: nil, - Precedence: nil, - Default: true, + GatewayUniqueID: StringPtr("t1235"), + SupportURL: StringPtr(""), + CaptivePortal: IntPtr(180), + AllowModeSwitch: BoolPtr(false), + SwitchLocked: BoolPtr(false), + AllowUpdates: BoolPtr(false), + AutoConnect: IntPtr(0), + AllowedToLeave: BoolPtr(true), + Enabled: BoolPtr(true), + PolicyID: nil, + Name: nil, + Match: nil, + Precedence: nil, + Default: true, + ExcludeOfficeIps: BoolPtr(false), } nonDefaultDeviceSettingsPolicy = DeviceSettingsPolicy{ @@ -56,20 +57,21 @@ var ( {Address: "10.0.0.0/8"}, {Address: "100.64.0.0/10"}, }, - GatewayUniqueID: StringPtr("t1235"), - SupportURL: StringPtr(""), - CaptivePortal: IntPtr(180), - AllowModeSwitch: BoolPtr(false), - SwitchLocked: BoolPtr(false), - AllowUpdates: BoolPtr(false), - AutoConnect: IntPtr(0), - AllowedToLeave: BoolPtr(true), - PolicyID: &deviceSettingsPolicyID, - Enabled: BoolPtr(true), - Name: StringPtr("test"), - Match: &deviceSettingsPolicyMatch, - Precedence: &deviceSettingsPolicyPrecedence, - Default: false, + GatewayUniqueID: StringPtr("t1235"), + SupportURL: StringPtr(""), + CaptivePortal: IntPtr(180), + AllowModeSwitch: BoolPtr(false), + SwitchLocked: BoolPtr(false), + AllowUpdates: BoolPtr(false), + AutoConnect: IntPtr(0), + AllowedToLeave: BoolPtr(true), + PolicyID: &deviceSettingsPolicyID, + Enabled: BoolPtr(true), + Name: StringPtr("test"), + Match: &deviceSettingsPolicyMatch, + Precedence: &deviceSettingsPolicyPrecedence, + Default: false, + ExcludeOfficeIps: BoolPtr(true), } defaultDeviceSettingsPolicyJson = `{ @@ -102,7 +104,8 @@ var ( "auto_connect": 0, "allowed_to_leave": true, "enabled": true, - "default": true + "default": true, + "exclude_office_ips":false }` nonDefaultDeviceSettingsPolicyJson = fmt.Sprintf(`{ @@ -139,7 +142,8 @@ var ( "name": "test", "match": %#v, "precedence": 10, - "default": false + "default": false, + "exclude_office_ips":true }`, deviceSettingsPolicyID, deviceSettingsPolicyMatch) ) diff --git a/dlp_payload_log.go b/dlp_payload_log.go new file mode 100644 index 0000000000..34f37ca8cc --- /dev/null +++ b/dlp_payload_log.go @@ -0,0 +1,71 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type DLPPayloadLogSettings struct { + PublicKey string `json:"public_key,omitempty"` + + // Only present in responses + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +type GetDLPPayloadLogSettingsParams struct{} + +type DLPPayloadLogSettingsResponse struct { + Response + Result DLPPayloadLogSettings `json:"result"` +} + +// GetDLPPayloadLogSettings gets the current DLP payload logging settings. +// +// API reference: https://api.cloudflare.com/#dlp-payload-log-settings-get-settings +func (api *API) GetDLPPayloadLogSettings(ctx context.Context, rc *ResourceContainer, params GetDLPPayloadLogSettingsParams) (DLPPayloadLogSettings, error) { + if rc.Identifier == "" { + return DLPPayloadLogSettings{}, ErrMissingResourceIdentifier + } + + uri := buildURI(fmt.Sprintf("/%s/%s/dlp/payload_log", rc.Level, rc.Identifier), nil) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return DLPPayloadLogSettings{}, err + } + + var dlpPayloadLogSettingsResponse DLPPayloadLogSettingsResponse + err = json.Unmarshal(res, &dlpPayloadLogSettingsResponse) + if err != nil { + return DLPPayloadLogSettings{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return dlpPayloadLogSettingsResponse.Result, nil +} + +// UpdateDLPPayloadLogSettings sets the current DLP payload logging settings to new values. +// +// API reference: https://api.cloudflare.com/#dlp-payload-log-settings-update-settings +func (api *API) UpdateDLPPayloadLogSettings(ctx context.Context, rc *ResourceContainer, settings DLPPayloadLogSettings) (DLPPayloadLogSettings, error) { + if rc.Identifier == "" { + return DLPPayloadLogSettings{}, ErrMissingResourceIdentifier + } + + uri := buildURI(fmt.Sprintf("/%s/%s/dlp/payload_log", rc.Level, rc.Identifier), nil) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, settings) + if err != nil { + return DLPPayloadLogSettings{}, err + } + + var dlpPayloadLogSettingsResponse DLPPayloadLogSettingsResponse + err = json.Unmarshal(res, &dlpPayloadLogSettingsResponse) + if err != nil { + return DLPPayloadLogSettings{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return dlpPayloadLogSettingsResponse.Result, nil +} diff --git a/dlp_payload_log_test.go b/dlp_payload_log_test.go new file mode 100644 index 0000000000..4ef505da49 --- /dev/null +++ b/dlp_payload_log_test.go @@ -0,0 +1,84 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetDLPPayloadLogSettings(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "public_key": "3NP5MGKjzBLLceVxNZrF+LyithbWX+AVFBMRAA0Xl2A=", + "updated_at": "2022-12-22T21:02:39Z" + } + }`) + } + + updatedAt, _ := time.Parse(time.RFC3339, "2022-12-22T21:02:39Z") + + want := DLPPayloadLogSettings{ + PublicKey: "3NP5MGKjzBLLceVxNZrF+LyithbWX+AVFBMRAA0Xl2A=", + UpdatedAt: &updatedAt, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/dlp/payload_log", handler) + + actual, err := client.GetDLPPayloadLogSettings(context.Background(), AccountIdentifier(testAccountID), GetDLPPayloadLogSettingsParams{}) + require.NoError(t, err) + require.Equal(t, want, actual) +} + +func TestPutDLPPayloadLogSettings(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) + w.Header().Set("content-type", "application/json") + + var requestSettings DLPPayloadLogSettings + err := json.NewDecoder(r.Body).Decode(&requestSettings) + require.Nil(t, err) + + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "public_key": "`+requestSettings.PublicKey+`", + "updated_at": "2022-12-22T21:02:39Z" + } + }`) + } + + updatedAt, _ := time.Parse(time.RFC3339, "2022-12-22T21:02:39Z") + + want := DLPPayloadLogSettings{ + PublicKey: "3NP5MGKjzBLLceVxNZrF+LyithbWX+AVFBMRAA0Xl2A=", + UpdatedAt: &updatedAt, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/dlp/payload_log", handler) + + actual, err := client.UpdateDLPPayloadLogSettings(context.Background(), AccountIdentifier(testAccountID), DLPPayloadLogSettings{ + PublicKey: "3NP5MGKjzBLLceVxNZrF+LyithbWX+AVFBMRAA0Xl2A=", + }) + require.NoError(t, err) + require.Equal(t, want, actual) +} diff --git a/dlp_profile.go b/dlp_profile.go index 94202842ee..4eb0b5ff5c 100644 --- a/dlp_profile.go +++ b/dlp_profile.go @@ -37,10 +37,11 @@ type DLPEntry struct { // DLPProfile represents a DLP Profile, which contains a set // of entries. type DLPProfile struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Description string `json:"description,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + AllowedMatchCount int `json:"allowed_match_count"` // The following fields are omitted for predefined DLP // profiles diff --git a/dlp_profile_test.go b/dlp_profile_test.go index d7c88f5718..20b4aa86c1 100644 --- a/dlp_profile_test.go +++ b/dlp_profile_test.go @@ -43,7 +43,8 @@ func TestDLPProfiles(t *testing.T) { "type": "predefined" } ], - "type": "predefined" + "type": "predefined", + "allowed_match_count": 0 }, { "id": "29678c26-a191-428d-9f63-6e20a4a636a4", @@ -66,7 +67,8 @@ func TestDLPProfiles(t *testing.T) { "created_at": "2022-10-18T08:00:56Z", "updated_at": "2022-10-18T08:00:57Z", "type": "custom", - "description": "just a custom profile example" + "description": "just a custom profile example", + "allowed_match_count": 1 } ] } @@ -78,10 +80,11 @@ func TestDLPProfiles(t *testing.T) { want := []DLPProfile{ { - ID: "d658f520-6ecb-4a34-a725-ba37243c2d28", - Name: "U.S. Social Security Numbers", - Type: "predefined", - Description: "", + ID: "d658f520-6ecb-4a34-a725-ba37243c2d28", + Name: "U.S. Social Security Numbers", + Type: "predefined", + Description: "", + AllowedMatchCount: 0, Entries: []DLPEntry{ { ID: "111b9d4b-a5c6-40f0-957d-9d53b25dd84a", @@ -99,10 +102,11 @@ func TestDLPProfiles(t *testing.T) { }, }, { - ID: "29678c26-a191-428d-9f63-6e20a4a636a4", - Name: "Example Custom Profile", - Type: "custom", - Description: "just a custom profile example", + ID: "29678c26-a191-428d-9f63-6e20a4a636a4", + Name: "Example Custom Profile", + Type: "custom", + Description: "just a custom profile example", + AllowedMatchCount: 1, Entries: []DLPEntry{ { ID: "ef79b054-12d4-4067-bb30-b85f6267b91c", @@ -161,7 +165,8 @@ func TestGetDLPProfile(t *testing.T) { "created_at": "2022-10-18T08:00:56Z", "updated_at": "2022-10-18T08:00:57Z", "type": "custom", - "description": "just a custom profile example" + "description": "just a custom profile example", + "allowed_match_count": 42 } }`) } @@ -170,10 +175,11 @@ func TestGetDLPProfile(t *testing.T) { updatedAt, _ := time.Parse(time.RFC3339, "2022-10-18T08:00:57Z") want := DLPProfile{ - ID: "29678c26-a191-428d-9f63-6e20a4a636a4", - Name: "Example Custom Profile", - Type: "custom", - Description: "just a custom profile example", + ID: "29678c26-a191-428d-9f63-6e20a4a636a4", + Name: "Example Custom Profile", + Type: "custom", + Description: "just a custom profile example", + AllowedMatchCount: 42, Entries: []DLPEntry{ { ID: "ef79b054-12d4-4067-bb30-b85f6267b91c", @@ -238,7 +244,8 @@ func TestCreateDLPCustomProfiles(t *testing.T) { "created_at": "2022-10-18T08:00:56Z", "updated_at": "2022-10-18T08:00:57Z", "type": "custom", - "description": "`+requestProfile.Description+`" + "description": "`+requestProfile.Description+`", + "allowed_match_count": 0 }] }`) } @@ -267,8 +274,9 @@ func TestCreateDLPCustomProfiles(t *testing.T) { UpdatedAt: &updatedAt, }, }, - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + AllowedMatchCount: 0, }, } @@ -289,6 +297,7 @@ func TestCreateDLPCustomProfiles(t *testing.T) { }, }, }, + AllowedMatchCount: 0, }, } actual, err := client.CreateDLPProfiles(context.Background(), AccountIdentifier(testAccountID), CreateDLPProfilesParams{Profiles: profiles, Type: "custom"}) @@ -334,7 +343,8 @@ func TestCreateDLPCustomProfile(t *testing.T) { "created_at": "2022-10-18T08:00:56Z", "updated_at": "2022-10-18T08:00:57Z", "type": "custom", - "description": "`+requestProfile.Description+`" + "description": "`+requestProfile.Description+`", + "allowed_match_count": 0 }] }`) } @@ -363,8 +373,9 @@ func TestCreateDLPCustomProfile(t *testing.T) { UpdatedAt: &updatedAt, }, }, - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + AllowedMatchCount: 0, }} mux.HandleFunc("/accounts/"+testAccountID+"/dlp/profiles/custom", handler) @@ -382,6 +393,7 @@ func TestCreateDLPCustomProfile(t *testing.T) { }, }, }, + AllowedMatchCount: 0, }} actual, err := client.CreateDLPProfiles(context.Background(), AccountIdentifier(testAccountID), CreateDLPProfilesParams{ @@ -429,7 +441,8 @@ func TestUpdateDLPCustomProfile(t *testing.T) { "created_at": "2022-10-18T08:00:56Z", "updated_at": "2022-10-18T08:00:57Z", "type": "custom", - "description": "`+requestProfile.Description+`" + "description": "`+requestProfile.Description+`", + "allowed_match_count": 0 } }`) } @@ -458,8 +471,9 @@ func TestUpdateDLPCustomProfile(t *testing.T) { UpdatedAt: &updatedAt, }, }, - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + AllowedMatchCount: 0, } mux.HandleFunc("/accounts/"+testAccountID+"/dlp/profiles/custom/29678c26-a191-428d-9f63-6e20a4a636a4", handler) @@ -477,6 +491,7 @@ func TestUpdateDLPCustomProfile(t *testing.T) { }, }, }, + AllowedMatchCount: 0, } actual, err := client.UpdateDLPProfile(context.Background(), AccountIdentifier(testAccountID), UpdateDLPProfileParams{ ProfileID: "29678c26-a191-428d-9f63-6e20a4a636a4", @@ -516,7 +531,8 @@ func TestUpdateDLPPredefinedProfile(t *testing.T) { } ], "type": "predefined", - "description": "example predefined profile" + "description": "example predefined profile", + "allowed_match_count": 0 } }`) } @@ -535,6 +551,7 @@ func TestUpdateDLPPredefinedProfile(t *testing.T) { Enabled: BoolPtr(true), }, }, + AllowedMatchCount: 0, } mux.HandleFunc("/accounts/"+testAccountID+"/dlp/profiles/predefined/29678c26-a191-428d-9f63-6e20a4a636a4", handler) diff --git a/dns.go b/dns.go index affb167efb..cee3b1d9de 100644 --- a/dns.go +++ b/dns.go @@ -1,17 +1,22 @@ package cloudflare import ( + "bytes" "context" "encoding/json" + "errors" "fmt" "net/http" - "net/url" - "strconv" + "regexp" + "strings" "time" "golang.org/x/net/idna" ) +// ErrMissingBINDContents is for when the BIND file contents is required but not set. +var ErrMissingBINDContents = errors.New("required BIND config contents missing") + // DNSRecord represents a DNS record in a zone. type DNSRecord struct { CreatedOn time.Time `json:"created_on,omitempty"` @@ -29,6 +34,8 @@ type DNSRecord struct { Proxied *bool `json:"proxied,omitempty"` Proxiable bool `json:"proxiable,omitempty"` Locked bool `json:"locked,omitempty"` + Comment string `json:"comment,omitempty"` + Tags []string `json:"tags,omitempty"` } // DNSRecordResponse represents the response from the DNS endpoint. @@ -38,6 +45,42 @@ type DNSRecordResponse struct { ResultInfo `json:"result_info"` } +type ListDirection string + +const ( + ListDirectionAsc ListDirection = "asc" + ListDirectionDesc ListDirection = "desc" +) + +type ListDNSRecordsParams struct { + Type string `url:"type,omitempty"` + Name string `url:"name,omitempty"` + Content string `url:"content,omitempty"` + Proxied *bool `url:"proxied,omitempty"` + Comment string `url:"comment,omitempty"` + Tags []string `url:"tag,omitempty"` // potentially multiple `tag=` + TagMatch string `url:"tag-match,omitempty"` + Order string `url:"order,omitempty"` + Direction ListDirection `url:"direction,omitempty"` + Match string `url:"match,omitempty"` + Priority *uint16 `url:"-"` + + ResultInfo +} + +type UpdateDNSRecordParams struct { + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Content string `json:"content,omitempty"` + Data interface{} `json:"data,omitempty"` // data for: SRV, LOC + ID string `json:"-"` + Priority *uint16 `json:"priority,omitempty"` + TTL int `json:"ttl,omitempty"` + Proxied *bool `json:"proxied,omitempty"` + Comment string `json:"comment"` + Tags []string `json:"tags"` +} + // DNSListResponse represents the response from the list DNS records endpoint. type DNSListResponse struct { Result []DNSRecord `json:"result"` @@ -45,6 +88,9 @@ type DNSListResponse struct { ResultInfo `json:"result_info"` } +// listDNSRecordsDefaultPageSize represents the default per_page size of the API. +var listDNSRecordsDefaultPageSize int = 100 + // nontransitionalLookup implements the nontransitional processing as specified in // Unicode Technical Standard 46 with almost all checkings off to maximize user freedom. var nontransitionalLookup = idna.New( @@ -64,78 +110,180 @@ func toUTS46ASCII(name string) string { return name } +// proxiedRecordsRe is the regular expression for determining if a DNS record +// is proxied or not. +var proxiedRecordsRe = regexp.MustCompile(`(?m)^.*\.\s+1\s+IN\s+CNAME.*$`) + +// proxiedRecordImportTemplate is the multipart template for importing *only* +// proxied records. See `nonProxiedRecordImportTemplate` for importing records +// that are not proxied. +var proxiedRecordImportTemplate = `--------------------------BOUNDARY +Content-Disposition: form-data; name="file"; filename="bind.txt" + +%s +--------------------------BOUNDARY +Content-Disposition: form-data; name="proxied" + +true +--------------------------BOUNDARY--` + +// nonProxiedRecordImportTemplate is the multipart template for importing DNS +// records that are not proxed. For importing proxied records, use +// `proxiedRecordImportTemplate`. +var nonProxiedRecordImportTemplate = `--------------------------BOUNDARY +Content-Disposition: form-data; name="file"; filename="bind.txt" + +%s +--------------------------BOUNDARY--` + +// sanitiseBINDFileInput accepts the BIND file as a string and removes parts +// that are not required for importing or would break the import (like SOA +// records). +func sanitiseBINDFileInput(s string) string { + // Remove SOA records. + soaRe := regexp.MustCompile(`(?m)[\r\n]+^.*IN\s+SOA.*$`) + s = soaRe.ReplaceAllString(s, "") + + // Remove all comments. + commentRe := regexp.MustCompile(`(?m)[\r\n]+^.*;;.*$`) + s = commentRe.ReplaceAllString(s, "") + + // Swap all the tabs to spaces. + r := strings.NewReplacer( + "\t", " ", + "\n\n", "\n", + ) + s = r.Replace(s) + s = strings.TrimSpace(s) + + return s +} + +// extractProxiedRecords accepts a BIND file (as a string) and returns only the +// proxied DNS records. +func extractProxiedRecords(s string) string { + proxiedOnlyRecords := proxiedRecordsRe.FindAllString(s, -1) + return strings.Join(proxiedOnlyRecords, "\n") +} + +// removeProxiedRecords accepts a BIND file (as a string) and returns the file +// contents without any proxied records included. +func removeProxiedRecords(s string) string { + return proxiedRecordsRe.ReplaceAllString(s, "") +} + +type ExportDNSRecordsParams struct{} +type ImportDNSRecordsParams struct { + BINDContents string +} + +type CreateDNSRecordParams struct { + CreatedOn time.Time `json:"created_on,omitempty" url:"created_on,omitempty"` + ModifiedOn time.Time `json:"modified_on,omitempty" url:"modified_on,omitempty"` + Type string `json:"type,omitempty" url:"type,omitempty"` + Name string `json:"name,omitempty" url:"name,omitempty"` + Content string `json:"content,omitempty" url:"content,omitempty"` + Meta interface{} `json:"meta,omitempty"` + Data interface{} `json:"data,omitempty"` // data returned by: SRV, LOC + ID string `json:"id,omitempty"` + ZoneID string `json:"zone_id,omitempty"` + ZoneName string `json:"zone_name,omitempty"` + Priority *uint16 `json:"priority,omitempty"` + TTL int `json:"ttl,omitempty"` + Proxied *bool `json:"proxied,omitempty" url:"proxied,omitempty"` + Proxiable bool `json:"proxiable,omitempty"` + Locked bool `json:"locked,omitempty"` + Comment string `json:"comment,omitempty" url:"comment,omitempty"` + Tags []string `json:"tags,omitempty"` +} + // CreateDNSRecord creates a DNS record for the zone identifier. // // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record -func (api *API) CreateDNSRecord(ctx context.Context, zoneID string, rr DNSRecord) (*DNSRecordResponse, error) { - rr.Name = toUTS46ASCII(rr.Name) +func (api *API) CreateDNSRecord(ctx context.Context, rc *ResourceContainer, params CreateDNSRecordParams) (DNSRecord, error) { + if rc.Identifier == "" { + return DNSRecord{}, ErrMissingZoneID + } + params.Name = toUTS46ASCII(params.Name) - uri := fmt.Sprintf("/zones/%s/dns_records", zoneID) - res, err := api.makeRequestContext(ctx, http.MethodPost, uri, rr) + uri := fmt.Sprintf("/zones/%s/dns_records", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) if err != nil { - return nil, err + return DNSRecord{}, err } var recordResp *DNSRecordResponse err = json.Unmarshal(res, &recordResp) if err != nil { - return nil, fmt.Errorf("%s: %w", errUnmarshalError, err) + return DNSRecord{}, fmt.Errorf("%s: %w", errUnmarshalError, err) } - return recordResp, nil + return recordResp.Result, nil } -// DNSRecords returns a slice of DNS records for the given zone identifier. -// -// This takes a DNSRecord to allow filtering of the results returned. +// ListDNSRecords returns a slice of DNS records for the given zone identifier. // // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records -func (api *API) DNSRecords(ctx context.Context, zoneID string, rr DNSRecord) ([]DNSRecord, error) { - // Construct a query string - v := url.Values{} - // Using default per_page value as specified by the API - if rr.Name != "" { - v.Set("name", toUTS46ASCII(rr.Name)) +func (api *API) ListDNSRecords(ctx context.Context, rc *ResourceContainer, params ListDNSRecordsParams) ([]DNSRecord, *ResultInfo, error) { + if rc.Identifier == "" { + return nil, nil, ErrMissingZoneID } - if rr.Type != "" { - v.Set("type", rr.Type) + + params.Name = toUTS46ASCII(params.Name) + + autoPaginate := true + if params.PerPage >= 1 || params.Page >= 1 { + autoPaginate = false } - if rr.Content != "" { - v.Set("content", rr.Content) + + if params.PerPage < 1 { + params.PerPage = listDNSRecordsDefaultPageSize + } + + if params.Page < 1 { + params.Page = 1 } var records []DNSRecord - page := 1 + var lastResultInfo ResultInfo - // Loop over makeRequest until what we've fetched all records for { - v.Set("page", strconv.Itoa(page)) - uri := fmt.Sprintf("/zones/%s/dns_records?%s", zoneID, v.Encode()) + uri := buildURI(fmt.Sprintf("/zones/%s/dns_records", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { - return []DNSRecord{}, err + return []DNSRecord{}, &ResultInfo{}, err } - var r DNSListResponse - err = json.Unmarshal(res, &r) + var listResponse DNSListResponse + err = json.Unmarshal(res, &listResponse) if err != nil { - return []DNSRecord{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + return []DNSRecord{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err) } - records = append(records, r.Result...) - if r.ResultInfo.Page >= r.ResultInfo.TotalPages { + records = append(records, listResponse.Result...) + lastResultInfo = listResponse.ResultInfo + params.ResultInfo = listResponse.ResultInfo.Next() + if params.ResultInfo.Done() || !autoPaginate { break } - // Loop around and fetch the next page - page++ } - return records, nil + return records, &lastResultInfo, nil } -// DNSRecord returns a single DNS record for the given zone & record +// ErrMissingDNSRecordID is for when DNS record ID is needed but not given. +var ErrMissingDNSRecordID = errors.New("required DNS record ID missing") + +// GetDNSRecord returns a single DNS record for the given zone & record // identifiers. // // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-dns-record-details -func (api *API) DNSRecord(ctx context.Context, zoneID, recordID string) (DNSRecord, error) { - uri := fmt.Sprintf("/zones/%s/dns_records/%s", zoneID, recordID) +func (api *API) GetDNSRecord(ctx context.Context, rc *ResourceContainer, recordID string) (DNSRecord, error) { + if rc.Identifier == "" { + return DNSRecord{}, ErrMissingZoneID + } + if recordID == "" { + return DNSRecord{}, ErrMissingDNSRecordID + } + + uri := fmt.Sprintf("/zones/%s/dns_records/%s", rc.Identifier, recordID) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return DNSRecord{}, err @@ -152,43 +300,45 @@ func (api *API) DNSRecord(ctx context.Context, zoneID, recordID string) (DNSReco // identifiers. // // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record -func (api *API) UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr DNSRecord) error { - rr.Name = toUTS46ASCII(rr.Name) - - // Populate the record name from the existing one if the update didn't - // specify it. - if rr.Name == "" || rr.Type == "" { - rec, err := api.DNSRecord(ctx, zoneID, recordID) - if err != nil { - return err - } +func (api *API) UpdateDNSRecord(ctx context.Context, rc *ResourceContainer, params UpdateDNSRecordParams) (DNSRecord, error) { + if rc.Identifier == "" { + return DNSRecord{}, ErrMissingZoneID + } - if rr.Name == "" { - rr.Name = rec.Name - } - if rr.Type == "" { - rr.Type = rec.Type - } + if params.ID == "" { + return DNSRecord{}, ErrMissingDNSRecordID } - uri := fmt.Sprintf("/zones/%s/dns_records/%s", zoneID, recordID) - res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, rr) + + params.Name = toUTS46ASCII(params.Name) + + uri := fmt.Sprintf("/zones/%s/dns_records/%s", rc.Identifier, params.ID) + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params) if err != nil { - return err + return DNSRecord{}, err } - var r DNSRecordResponse - err = json.Unmarshal(res, &r) + + var recordResp *DNSRecordResponse + err = json.Unmarshal(res, &recordResp) if err != nil { - return fmt.Errorf("%s: %w", errUnmarshalError, err) + return DNSRecord{}, fmt.Errorf("%s: %w", errUnmarshalError, err) } - return nil + + return recordResp.Result, nil } // DeleteDNSRecord deletes a single DNS record for the given zone & record // identifiers. // // API reference: https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record -func (api *API) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error { - uri := fmt.Sprintf("/zones/%s/dns_records/%s", zoneID, recordID) +func (api *API) DeleteDNSRecord(ctx context.Context, rc *ResourceContainer, recordID string) error { + if rc.Identifier == "" { + return ErrMissingZoneID + } + if recordID == "" { + return ErrMissingDNSRecordID + } + + uri := fmt.Sprintf("/zones/%s/dns_records/%s", rc.Identifier, recordID) res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) if err != nil { return err @@ -200,3 +350,76 @@ func (api *API) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) er } return nil } + +// ExportDNSRecords returns all DNS records for a zone in the BIND format. +// +// API reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-export-dns-records +func (api *API) ExportDNSRecords(ctx context.Context, rc *ResourceContainer, params ExportDNSRecordsParams) (string, error) { + if rc.Level != ZoneRouteLevel { + return "", ErrRequiredZoneLevelResourceContainer + } + + if rc.Identifier == "" { + return "", ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/dns_records/export", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return "", err + } + + return string(res), nil +} + +// ImportDNSRecords takes the contents of a BIND configuration file and imports +// all records at once. +// +// The current state of the API doesn't allow the proxying field to be +// automatically set on records where the TTL is 1. Instead you need to +// explicitly tell the endpoint which records are proxied in the form data. To +// achieve a simpler abstraction, we do the legwork in the method of making the +// two separate API calls (one for proxied and one for non-proxied) instead of +// making the end user know about this detail. +// +// API reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-import-dns-records +func (api *API) ImportDNSRecords(ctx context.Context, rc *ResourceContainer, params ImportDNSRecordsParams) error { + if rc.Level != ZoneRouteLevel { + return ErrRequiredZoneLevelResourceContainer + } + + if rc.Identifier == "" { + return ErrMissingZoneID + } + + if params.BINDContents == "" { + return ErrMissingBINDContents + } + + sanitisedBindData := sanitiseBINDFileInput(params.BINDContents) + nonProxiedRecords := removeProxiedRecords(sanitisedBindData) + proxiedOnlyRecords := extractProxiedRecords(sanitisedBindData) + + nonProxiedRecordPayload := []byte(fmt.Sprintf(nonProxiedRecordImportTemplate, nonProxiedRecords)) + nonProxiedReqBody := bytes.NewReader(nonProxiedRecordPayload) + + uri := fmt.Sprintf("/zones/%s/dns_records/import", rc.Identifier) + multipartUploadHeaders := http.Header{ + "Content-Type": {"multipart/form-data; boundary=------------------------BOUNDARY"}, + } + + _, err := api.makeRequestContextWithHeaders(ctx, http.MethodPost, uri, nonProxiedReqBody, multipartUploadHeaders) + if err != nil { + return err + } + + proxiedRecordPayload := []byte(fmt.Sprintf(proxiedRecordImportTemplate, proxiedOnlyRecords)) + proxiedReqBody := bytes.NewReader(proxiedRecordPayload) + + _, err = api.makeRequestContextWithHeaders(ctx, http.MethodPost, uri, proxiedReqBody, multipartUploadHeaders) + if err != nil { + return err + } + + return nil +} diff --git a/dns_example_test.go b/dns_example_test.go index 8f0a5f33c7..e864a16fea 100644 --- a/dns_example_test.go +++ b/dns_example_test.go @@ -5,10 +5,10 @@ import ( "fmt" "log" - cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go" ) -func ExampleAPI_DNSRecords_all() { +func ExampleAPI_ListDNSRecords_all() { api, err := cloudflare.New("deadbeef", "test@example.org") if err != nil { log.Fatal(err) @@ -20,7 +20,7 @@ func ExampleAPI_DNSRecords_all() { } // Fetch all records for a zone - recs, err := api.DNSRecords(context.Background(), zoneID, cloudflare.DNSRecord{}) + recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{}) if err != nil { log.Fatal(err) } @@ -30,7 +30,7 @@ func ExampleAPI_DNSRecords_all() { } } -func ExampleAPI_DNSRecords_filterByContent() { +func ExampleAPI_ListDNSRecords_filterByContent() { api, err := cloudflare.New("deadbeef", "test@example.org") if err != nil { log.Fatal(err) @@ -41,9 +41,7 @@ func ExampleAPI_DNSRecords_filterByContent() { log.Fatal(err) } - // Fetch only records whose content is 198.51.100.1 - localhost := cloudflare.DNSRecord{Content: "198.51.100.1"} - recs, err := api.DNSRecords(context.Background(), zoneID, localhost) + recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Content: "198.51.100.1"}) if err != nil { log.Fatal(err) } @@ -53,7 +51,7 @@ func ExampleAPI_DNSRecords_filterByContent() { } } -func ExampleAPI_DNSRecords_filterByName() { +func ExampleAPI_ListDNSRecords_filterByName() { api, err := cloudflare.New("deadbeef", "test@example.org") if err != nil { log.Fatal(err) @@ -64,10 +62,7 @@ func ExampleAPI_DNSRecords_filterByName() { log.Fatal(err) } - // Fetch records of any type with name "foo.example.com" - // The name must be fully-qualified - foo := cloudflare.DNSRecord{Name: "foo.example.com"} - recs, err := api.DNSRecords(context.Background(), zoneID, foo) + recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Name: "foo.example.com"}) if err != nil { log.Fatal(err) } @@ -77,7 +72,7 @@ func ExampleAPI_DNSRecords_filterByName() { } } -func ExampleAPI_DNSRecords_filterByType() { +func ExampleAPI_ListDNSRecords_filterByType() { api, err := cloudflare.New("deadbeef", "test@example.org") if err != nil { log.Fatal(err) @@ -88,9 +83,7 @@ func ExampleAPI_DNSRecords_filterByType() { log.Fatal(err) } - // Fetch only AAAA type records - aaaa := cloudflare.DNSRecord{Type: "AAAA"} - recs, err := api.DNSRecords(context.Background(), zoneID, aaaa) + recs, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{Type: "AAAA"}) if err != nil { log.Fatal(err) } diff --git a/dns_firewall.go b/dns_firewall.go index f2cfa42ae8..a6282aea20 100644 --- a/dns_firewall.go +++ b/dns_firewall.go @@ -14,7 +14,7 @@ import ( type DNSFirewallCluster struct { ID string `json:"id,omitempty"` Name string `json:"name"` - OriginIPs []string `json:"origin_ips"` + UpstreamIPs []string `json:"upstream_ips"` DNSFirewallIPs []string `json:"dns_firewall_ips,omitempty"` MinimumCacheTTL uint `json:"minimum_cache_ttl,omitempty"` MaximumCacheTTL uint `json:"maximum_cache_ttl,omitempty"` @@ -166,10 +166,10 @@ func (api *API) DeleteDNSFirewallCluster(ctx context.Context, clusterID string) func (o DNSFirewallUserAnalyticsOptions) encode() string { v := url.Values{} if o.Since != nil { - v.Set("since", (*o.Since).UTC().Format(time.RFC3339)) + v.Set("since", o.Since.UTC().Format(time.RFC3339)) } if o.Until != nil { - v.Set("until", (*o.Until).UTC().Format(time.RFC3339)) + v.Set("until", o.Until.UTC().Format(time.RFC3339)) } if o.Metrics != nil { v.Set("metrics", strings.Join(o.Metrics, ",")) diff --git a/dns_test.go b/dns_test.go index b2cc16fdb0..3145a764c8 100644 --- a/dns_test.go +++ b/dns_test.go @@ -68,14 +68,6 @@ func TestCreateDNSRecord(t *testing.T) { priority := uint16(10) proxied := false - unicodeInput := DNSRecord{ - Type: "A", - Name: "😺.example.com", - Content: "198.51.100.4", - TTL: 120, - Priority: &priority, - Proxied: &proxied, - } asciiInput := DNSRecord{ Type: "A", Name: "xn--138h.example.com", @@ -124,43 +116,44 @@ func TestCreateDNSRecord(t *testing.T) { createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") - want := &DNSRecordResponse{ - Result: DNSRecord{ - ID: "372e67954025e0ba6aaa6d586b9e0b59", - Type: asciiInput.Type, - Name: asciiInput.Name, - Content: asciiInput.Content, - Proxiable: true, - Proxied: asciiInput.Proxied, - TTL: asciiInput.TTL, - ZoneID: testZoneID, - ZoneName: "example.com", - CreatedOn: createdOn, - ModifiedOn: modifiedOn, - Data: map[string]interface{}{}, - Meta: map[string]interface{}{ - "auto_added": true, - "source": "primary", - }, + want := DNSRecord{ + ID: "372e67954025e0ba6aaa6d586b9e0b59", + Type: asciiInput.Type, + Name: asciiInput.Name, + Content: asciiInput.Content, + Proxiable: true, + Proxied: asciiInput.Proxied, + TTL: asciiInput.TTL, + ZoneID: testZoneID, + ZoneName: "example.com", + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + Data: map[string]interface{}{}, + Meta: map[string]interface{}{ + "auto_added": true, + "source": "primary", }, - Response: Response{Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}}, } - actual, err := client.CreateDNSRecord(context.Background(), testZoneID, unicodeInput) + _, err := client.CreateDNSRecord(context.Background(), ZoneIdentifier(""), CreateDNSRecordParams{}) + assert.ErrorIs(t, err, ErrMissingZoneID) + + actual, err := client.CreateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), CreateDNSRecordParams{ + Type: "A", + Name: "😺.example.com", + Content: "198.51.100.4", + TTL: 120, + Priority: &priority, + Proxied: &proxied}) require.NoError(t, err) assert.Equal(t, want, actual) } -func TestDNSRecords(t *testing.T) { +func TestListDNSRecords(t *testing.T) { setup() defer teardown() - unicodeInput := DNSRecord{ - Name: "😺.example.com", - Type: "A", - Content: "198.51.100.4", - } asciiInput := DNSRecord{ Name: "xn--138h.example.com", Type: "A", @@ -200,8 +193,10 @@ func TestDNSRecords(t *testing.T) { } ], "result_info": { + "count": 1, "page": 1, - "total_pages": 1 + "per_page": 20, + "total_count": 2000 } }`) } @@ -231,61 +226,91 @@ func TestDNSRecords(t *testing.T) { }, }} - actual, err := client.DNSRecords(context.Background(), testZoneID, unicodeInput) + _, _, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(""), ListDNSRecordsParams{}) + assert.ErrorIs(t, err, ErrMissingZoneID) + + actual, _, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(testZoneID), ListDNSRecordsParams{ + Name: "😺.example.com", + Type: "A", + Content: "198.51.100.4", + }) require.NoError(t, err) assert.Equal(t, want, actual) } -func TestDNSRecord(t *testing.T) { +func TestListDNSRecordsSearch(t *testing.T) { setup() defer teardown() + recordInput := DNSRecord{ + Name: "example.com", + Type: "A", + Content: "198.51.100.4", + } + handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + assert.Equal(t, recordInput.Name, r.URL.Query().Get("name")) + assert.Equal(t, recordInput.Type, r.URL.Query().Get("type")) + assert.Equal(t, recordInput.Content, r.URL.Query().Get("content")) + assert.Equal(t, "all", r.URL.Query().Get("match")) + assert.Equal(t, "1", r.URL.Query().Get("page")) + assert.Equal(t, "type", r.URL.Query().Get("order")) + assert.Equal(t, "asc", r.URL.Query().Get("direction")) + assert.Equal(t, "any", r.URL.Query().Get("tag-match")) + assert.ElementsMatch(t, []string{"tag1", "tag2"}, r.URL.Query()["tag"]) w.Header().Set("content-type", "application/json") fmt.Fprint(w, `{ "success": true, "errors": [], "messages": [], - "result": { - "id": "372e67954025e0ba6aaa6d586b9e0b59", - "type": "A", - "name": "example.com", - "content": "198.51.100.4", - "proxiable": true, - "proxied": false, - "ttl": 120, - "locked": false, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", - "created_on": "2014-01-01T05:20:00Z", - "modified_on": "2014-01-01T05:20:00Z", - "data": {}, - "meta": { - "auto_added": true, - "source": "primary" + "result": [ + { + "id": "372e67954025e0ba6aaa6d586b9e0b59", + "type": "A", + "name": "example.com", + "content": "198.51.100.4", + "proxiable": true, + "proxied": true, + "ttl": 120, + "locked": false, + "zone_id": "d56084adb405e0b7e32c52321bf07be6", + "zone_name": "example.com", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "data": {}, + "meta": { + "auto_added": true, + "source": "primary" + }, + "tags": ["tag1", "tag2extended"] } + ], + "result_info": { + "count": 1, + "page": 1, + "per_page": 20, + "total_count": 2000 } }`) } - dnsRecordID := "372e67954025e0ba6aaa6d586b9e0b59" - - mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler) + mux.HandleFunc("/zones/"+testZoneID+"/dns_records", handler) - proxied := false + proxied := true createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") - want := DNSRecord{ - ID: dnsRecordID, + want := []DNSRecord{{ + ID: "372e67954025e0ba6aaa6d586b9e0b59", Type: "A", - Name: "example.com", - Content: "198.51.100.4", + Name: recordInput.Name, + Content: recordInput.Content, Proxiable: true, Proxied: &proxied, TTL: 120, + Locked: false, ZoneID: testZoneID, ZoneName: "example.com", CreatedOn: createdOn, @@ -295,34 +320,108 @@ func TestDNSRecord(t *testing.T) { "auto_added": true, "source": "primary", }, - } + Tags: []string{"tag1", "tag2extended"}, + }} - actual, err := client.DNSRecord(context.Background(), testZoneID, dnsRecordID) + actual, resultInfo, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(testZoneID), ListDNSRecordsParams{ + ResultInfo: ResultInfo{ + Page: 1, + }, + Match: "all", + Order: "type", + Direction: ListDirectionAsc, + Name: "example.com", + Type: "A", + Content: "198.51.100.4", + TagMatch: "any", + Tags: []string{"tag1", "tag2"}, + }) require.NoError(t, err) + assert.Equal(t, 2000, resultInfo.Total) assert.Equal(t, want, actual) } -func TestUpdateDNSRecord(t *testing.T) { +func TestListDNSRecordsPagination(t *testing.T) { + // change listDNSRecordsDefaultPageSize value to 1 to force pagination + listDNSRecordsDefaultPageSize = 3 + setup() defer teardown() - proxied := false - input := DNSRecord{ - Type: "A", - Name: "example.com", - Content: "198.51.100.4", - TTL: 120, - Proxied: &proxied, + var page1Called, page2Called bool + handler := func(w http.ResponseWriter, r *http.Request) { + page := r.URL.Query().Get("page") + w.Header().Set("content-type", "application/json") + + var response string + switch page { + case "1": + response = loadFixture("dns", "list_page_1") + page1Called = true + case "2": + response = loadFixture("dns", "list_page_2") + page2Called = true + default: + assert.Failf(t, "Unexpeted page requested: %s", page) + return + } + fmt.Fprint(w, response) } - handler := func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + mux.HandleFunc("/zones/"+testZoneID+"/dns_records", handler) - var v DNSRecord - err := json.NewDecoder(r.Body).Decode(&v) - require.NoError(t, err) - assert.Equal(t, input, v) + actual, _, err := client.ListDNSRecords(context.Background(), ZoneIdentifier(testZoneID), ListDNSRecordsParams{}) + require.NoError(t, err) + assert.True(t, page1Called) + assert.True(t, page2Called) + assert.Len(t, actual, 5) + + type ls struct { + Results []map[string]interface{} `json:"result"` + } + + expectedRecords := make(map[string]map[string]interface{}) + + response1 := loadFixture("dns", "list_page_1") + var fixtureDataPage1 ls + err = json.Unmarshal([]byte(response1), &fixtureDataPage1) + assert.NoError(t, err) + for _, record := range fixtureDataPage1.Results { + expectedRecords[record["id"].(string)] = record + } + + response2 := loadFixture("dns", "list_page_2") + var fixtureDataPage2 ls + err = json.Unmarshal([]byte(response2), &fixtureDataPage2) + assert.NoError(t, err) + for _, record := range fixtureDataPage2.Results { + expectedRecords[record["id"].(string)] = record + } + + for _, actualRecord := range actual { + expected, exist := expectedRecords[actualRecord.ID] + assert.True(t, exist, "DNS record doesn't exist in fixtures") + assert.Equal(t, expected["type"].(string), actualRecord.Type) + assert.Equal(t, expected["name"].(string), actualRecord.Name) + assert.Equal(t, expected["content"].(string), actualRecord.Content) + assert.Equal(t, expected["proxiable"].(bool), actualRecord.Proxiable) + assert.Equal(t, expected["proxied"].(bool), *actualRecord.Proxied) + assert.Equal(t, int(expected["ttl"].(float64)), actualRecord.TTL) + assert.Equal(t, expected["locked"].(bool), actualRecord.Locked) + assert.Equal(t, expected["zone_id"].(string), actualRecord.ZoneID) + assert.Equal(t, expected["zone_name"].(string), actualRecord.ZoneName) + assert.Equal(t, expected["data"], actualRecord.Data) + assert.Equal(t, expected["meta"], actualRecord.Meta) + } +} + +func TestGetDNSRecord(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ @@ -346,7 +445,9 @@ func TestUpdateDNSRecord(t *testing.T) { "meta": { "auto_added": true, "source": "primary" - } + }, + "comment": "This is a comment", + "tags": ["tag1", "tag2"] } }`) } @@ -355,69 +456,64 @@ func TestUpdateDNSRecord(t *testing.T) { mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler) - err := client.UpdateDNSRecord(context.Background(), testZoneID, dnsRecordID, input) + proxied := false + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + want := DNSRecord{ + ID: dnsRecordID, + Type: "A", + Name: "example.com", + Content: "198.51.100.4", + Proxiable: true, + Proxied: &proxied, + TTL: 120, + ZoneID: testZoneID, + ZoneName: "example.com", + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + Data: map[string]interface{}{}, + Meta: map[string]interface{}{ + "auto_added": true, + "source": "primary", + }, + Comment: "This is a comment", + Tags: []string{"tag1", "tag2"}, + } + + _, err := client.GetDNSRecord(context.Background(), ZoneIdentifier(""), dnsRecordID) + assert.ErrorIs(t, err, ErrMissingZoneID) + + _, err = client.GetDNSRecord(context.Background(), ZoneIdentifier(testZoneID), "") + assert.ErrorIs(t, err, ErrMissingDNSRecordID) + + actual, err := client.GetDNSRecord(context.Background(), ZoneIdentifier(testZoneID), dnsRecordID) require.NoError(t, err) + + assert.Equal(t, want, actual) } -func TestUpdateDNSRecordWithoutName(t *testing.T) { +func TestUpdateDNSRecord(t *testing.T) { setup() defer teardown() proxied := false - - asciiInput := DNSRecord{ - Name: "xn--138h.example.com", - Type: "A", - Content: "198.51.100.4", - TTL: 120, - Proxied: &proxied, - } - - unicodeInput := DNSRecord{ - Name: "😺.example.com", + input := DNSRecord{ + ID: "372e67954025e0ba6aaa6d586b9e0b59", Type: "A", + Name: "xn--138h.example.com", Content: "198.51.100.4", TTL: 120, Proxied: &proxied, } - handleUpdateDNSRecord := func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) var v DNSRecord err := json.NewDecoder(r.Body).Decode(&v) require.NoError(t, err) - assert.Equal(t, asciiInput, v) - - w.Header().Set("content-type", "application/json") - fmt.Fprint(w, `{ - "success": true, - "errors": [], - "messages": [], - "result": { - "id": "372e67954025e0ba6aaa6d586b9e0b59", - "type": "A", - "name": "xn--138h.example.com", - "content": "198.51.100.4", - "proxiable": true, - "proxied": false, - "ttl": 120, - "locked": false, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", - "created_on": "2014-01-01T05:20:00Z", - "modified_on": "2014-01-01T05:20:00Z", - "data": {}, - "meta": { - "auto_added": true, - "source": "primary" - } - } - }`) - } - - handleDNSRecord := func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + v.ID = "372e67954025e0ba6aaa6d586b9e0b59" + assert.Equal(t, input, v) w.Header().Set("content-type", "application/json") fmt.Fprint(w, `{ @@ -427,7 +523,7 @@ func TestUpdateDNSRecordWithoutName(t *testing.T) { "result": { "id": "372e67954025e0ba6aaa6d586b9e0b59", "type": "A", - "name": "xn--138h.example.com", + "name": "example.com", "content": "198.51.100.4", "proxiable": true, "proxied": false, @@ -446,86 +542,44 @@ func TestUpdateDNSRecordWithoutName(t *testing.T) { }`) } - handler := func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - handleDNSRecord(w, r) - return - } - - if r.Method == http.MethodPatch { - handleUpdateDNSRecord(w, r) - return - } - - assert.Failf(t, "Expected method 'GET' or `PATCH`, got %s", r.Method) - } - dnsRecordID := "372e67954025e0ba6aaa6d586b9e0b59" mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler) - err := client.UpdateDNSRecord(context.Background(), testZoneID, dnsRecordID, unicodeInput) - require.NoError(t, err) -} + _, err := client.UpdateDNSRecord(context.Background(), ZoneIdentifier(""), UpdateDNSRecordParams{ID: dnsRecordID}) + assert.ErrorIs(t, err, ErrMissingZoneID) -func TestUpdateDNSRecordWithoutType(t *testing.T) { - setup() - defer teardown() - - proxied := false + _, err = client.UpdateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), UpdateDNSRecordParams{}) + assert.ErrorIs(t, err, ErrMissingDNSRecordID) - unicodeInput := DNSRecord{ + _, err = client.UpdateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), UpdateDNSRecordParams{ + ID: dnsRecordID, + Type: "A", Name: "😺.example.com", Content: "198.51.100.4", TTL: 120, Proxied: &proxied, - } + }) + require.NoError(t, err) +} - completedASCIIInput := DNSRecord{ - Name: "xn--138h.example.com", - Type: "A", - Content: "198.51.100.4", - TTL: 120, - Proxied: &proxied, +func TestUpdateDNSRecord_ClearComment(t *testing.T) { + setup() + defer teardown() + + input := DNSRecord{ + ID: "372e67954025e0ba6aaa6d586b9e0b59", + Comment: "", } - handleUpdateDNSRecord := func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) var v DNSRecord err := json.NewDecoder(r.Body).Decode(&v) require.NoError(t, err) - assert.Equal(t, completedASCIIInput, v) - - w.Header().Set("content-type", "application/json") - fmt.Fprint(w, `{ - "success": true, - "errors": [], - "messages": [], - "result": { - "id": "372e67954025e0ba6aaa6d586b9e0b59", - "type": "A", - "name": "example.com", - "content": "198.51.100.4", - "proxiable": true, - "proxied": false, - "ttl": 120, - "locked": false, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", - "created_on": "2014-01-01T05:20:00Z", - "modified_on": "2014-01-01T05:20:00Z", - "data": {}, - "meta": { - "auto_added": true, - "source": "primary" - } - } - }`) - } - - handleDNSRecord := func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + v.ID = "372e67954025e0ba6aaa6d586b9e0b59" + assert.Equal(t, input, v) w.Header().Set("content-type", "application/json") fmt.Fprint(w, `{ @@ -545,6 +599,8 @@ func TestUpdateDNSRecordWithoutType(t *testing.T) { "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", + "comment":null, + "tags":[], "data": {}, "meta": { "auto_added": true, @@ -554,25 +610,14 @@ func TestUpdateDNSRecordWithoutType(t *testing.T) { }`) } - handler := func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - handleDNSRecord(w, r) - return - } - - if r.Method == http.MethodPatch { - handleUpdateDNSRecord(w, r) - return - } - - assert.Failf(t, "Expected method 'GET' or `PATCH`, got %s", r.Method) - } - dnsRecordID := "372e67954025e0ba6aaa6d586b9e0b59" mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler) - err := client.UpdateDNSRecord(context.Background(), testZoneID, dnsRecordID, unicodeInput) + _, err := client.UpdateDNSRecord(context.Background(), ZoneIdentifier(testZoneID), UpdateDNSRecordParams{ + ID: dnsRecordID, + Comment: "", + }) require.NoError(t, err) } @@ -598,6 +643,12 @@ func TestDeleteDNSRecord(t *testing.T) { mux.HandleFunc("/zones/"+testZoneID+"/dns_records/"+dnsRecordID, handler) - err := client.DeleteDNSRecord(context.Background(), testZoneID, dnsRecordID) + err := client.DeleteDNSRecord(context.Background(), ZoneIdentifier(""), dnsRecordID) + assert.ErrorIs(t, err, ErrMissingZoneID) + + err = client.DeleteDNSRecord(context.Background(), ZoneIdentifier(testZoneID), "") + assert.ErrorIs(t, err, ErrMissingDNSRecordID) + + err = client.DeleteDNSRecord(context.Background(), ZoneIdentifier(testZoneID), dnsRecordID) require.NoError(t, err) } diff --git a/docs/release-process.md b/docs/release-process.md index 181b92c23d..d2ae372d8d 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -2,9 +2,14 @@ We aim to release on a fortnightly cadence, alternating weeks with the [terraform-provider-cloudflare](https://github.com/cloudflare/terraform-provider-cloudflare). -This is to accommodate downstream tools and allow cahnges from this library to +This is to accommodate downstream tools and allow changes from this library to be used in the other systems without a month long delay. +To determine when the next release is due, you can either: + +- Review the latest [releases](https://github.com/cloudflare/cloudflare-go/releases); or +- Review the [current milestones](https://github.com/cloudflare/cloudflare-go/milestones). + If a hotfix is needed, the same process outlined below is used however only the semantic versioning patch version is bumped. diff --git a/email_routing_destination.go b/email_routing_destination.go index e9705e5590..cb9e30a945 100644 --- a/email_routing_destination.go +++ b/email_routing_destination.go @@ -61,6 +61,7 @@ func (api *API) ListEmailRoutingDestinationAddresses(ctx context.Context, rc *Re var addresses []EmailRoutingDestinationAddress var eResponse ListEmailRoutingAddressResponse for { + eResponse = ListEmailRoutingAddressResponse{} uri := buildURI(fmt.Sprintf("/accounts/%s/email/routing/addresses", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/email_routing_rules.go b/email_routing_rules.go index 22101b6245..a6c075f826 100644 --- a/email_routing_rules.go +++ b/email_routing_rules.go @@ -103,6 +103,7 @@ func (api *API) ListEmailRoutingRules(ctx context.Context, rc *ResourceContainer var rules []EmailRoutingRule var rResponse ListEmailRoutingRuleResponse for { + rResponse = ListEmailRoutingRuleResponse{} uri := buildURI(fmt.Sprintf("/zones/%s/email/routing/rules", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/errors.go b/errors.go index 994707978e..85fdc3efac 100644 --- a/errors.go +++ b/errors.go @@ -29,7 +29,9 @@ const ( errAPIKeysAndTokensAreMutuallyExclusive = "API keys and tokens are mutually exclusive" //nolint:gosec errMissingCredentials = "no credentials provided" - errInvalidResourceContainerAccess = "requested resource container (%q) is not supported for this endpoint" + errInvalidResourceContainerAccess = "requested resource container (%q) is not supported for this endpoint" + errRequiredAccountLevelResourceContainer = "this endpoint requires using an account level resource container and identifiers" + errRequiredZoneLevelResourceContainer = "this endpoint requires using a zone level resource container and identifiers" ) var ( @@ -40,6 +42,9 @@ var ( ErrAccountIDOrZoneIDAreRequired = errors.New(errMissingAccountOrZoneID) ErrAccountIDAndZoneIDAreMutuallyExclusive = errors.New(errAccountIDAndZoneIDAreMutuallyExclusive) ErrMissingResourceIdentifier = errors.New(errMissingResourceIdentifier) + + ErrRequiredAccountLevelResourceContainer = errors.New(errRequiredAccountLevelResourceContainer) + ErrRequiredZoneLevelResourceContainer = errors.New(errRequiredZoneLevelResourceContainer) ) type ErrorType string diff --git a/example_test.go b/example_test.go index 3320d363f2..5c6cc83e4f 100644 --- a/example_test.go +++ b/example_test.go @@ -28,7 +28,7 @@ func Example() { } // Fetch all DNS records for example.org - records, err := api.DNSRecords(context.Background(), zoneID, cloudflare.DNSRecord{}) + records, _, err := api.ListDNSRecords(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListDNSRecordsParams{}) if err != nil { fmt.Println(err) return diff --git a/filter.go b/filter.go index 904b6b055f..b5f084648a 100644 --- a/filter.go +++ b/filter.go @@ -124,6 +124,7 @@ func (api *API) Filters(ctx context.Context, rc *ResourceContainer, params Filte var filters []Filter var fResponse FiltersDetailResponse for { + fResponse = FiltersDetailResponse{} uri := buildURI(fmt.Sprintf("/zones/%s/filters", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/firewall_rules.go b/firewall_rules.go index 4d5636c379..51b4ae87ec 100644 --- a/firewall_rules.go +++ b/firewall_rules.go @@ -90,6 +90,7 @@ func (api *API) FirewallRules(ctx context.Context, rc *ResourceContainer, params var firewallRules []FirewallRule var fResponse FirewallRulesDetailResponse for { + fResponse = FirewallRulesDetailResponse{} uri := buildURI(fmt.Sprintf("/zones/%s/firewall/rules", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..153b6ef7dd --- /dev/null +++ b/flake.lock @@ -0,0 +1,540 @@ +{ + "nodes": { + "builtfilter": { + "inputs": { + "flox-floxpkgs": [ + "flox-floxpkgs" + ] + }, + "locked": { + "lastModified": 1679586988, + "narHash": "sha256-TkoF4E4yN40s042YG6DaOzk+vtbzsoey0lUO+tm03is=", + "owner": "flox", + "repo": "builtfilter", + "rev": "931a381bb96d909f51fcf25d7ca4fa9dcfb12aff", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "builtfilter-rs", + "repo": "builtfilter", + "type": "github" + } + }, + "capacitor": { + "inputs": { + "nixpkgs": "nixpkgs", + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1675110136, + "narHash": "sha256-83n/ZLBMoIkgYGy12F1hNaqMUgJsfkno5P1+sm9liOU=", + "owner": "flox", + "repo": "capacitor", + "rev": "9d4b9bce0f439e01fe2c2b2a1bfe08592a6204c4", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "v0", + "repo": "capacitor", + "type": "github" + } + }, + "capacitor_2": { + "inputs": { + "nixpkgs": [ + "flox-floxpkgs", + "nixpkgs", + "nixpkgs" + ], + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1675110136, + "narHash": "sha256-83n/ZLBMoIkgYGy12F1hNaqMUgJsfkno5P1+sm9liOU=", + "owner": "flox", + "repo": "capacitor", + "rev": "9d4b9bce0f439e01fe2c2b2a1bfe08592a6204c4", + "type": "github" + }, + "original": { + "owner": "flox", + "repo": "capacitor", + "type": "github" + } + }, + "catalog": { + "flake": false, + "locked": { + "lastModified": 1665076737, + "narHash": "sha256-S0bD7Z434Lvm7U4VHwvmxdTMrexdr72Yk6z0ExE3j7s=", + "owner": "flox", + "repo": "floxpkgs", + "rev": "bd8326c2fea27d01933eacb922f5ae70f97140c6", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "publish", + "repo": "floxpkgs", + "type": "github" + } + }, + "commitizen-src": { + "flake": false, + "locked": { + "lastModified": 1679149521, + "narHash": "sha256-F/fbBEwG7ijHELy4RnpvlXOPkTsXFxjALdK7UIGIzMo=", + "owner": "commitizen-tools", + "repo": "commitizen", + "rev": "378a42881891633d8a81939cb46426eb36ed01aa", + "type": "github" + }, + "original": { + "owner": "commitizen-tools", + "repo": "commitizen", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flox": { + "inputs": { + "commitizen-src": "commitizen-src", + "flox-bash": [ + "flox-floxpkgs", + "flox-bash" + ], + "flox-floxpkgs": [ + "flox-floxpkgs" + ], + "shellHooks": "shellHooks" + }, + "locked": { + "lastModified": 1679679655, + "narHash": "sha256-zug5lg3CqDMkmtbort6CHFJgUPyzRip0tg7t3dbSTWo=", + "ref": "main", + "rev": "98a44ab06d9f9f5e4824fd3add2d32aef95e606f", + "revCount": 427, + "type": "git", + "url": "ssh://git@github.com/flox/flox" + }, + "original": { + "ref": "main", + "type": "git", + "url": "ssh://git@github.com/flox/flox" + } + }, + "flox-bash": { + "inputs": { + "flox-floxpkgs": [ + "flox-floxpkgs" + ] + }, + "locked": { + "lastModified": 1679665116, + "narHash": "sha256-v1qgb6rOVa9q8oIlps/Z4kw7+YQqN3AsScNEgy5xwGQ=", + "ref": "main", + "rev": "db7efcacb5f5935b286006288f23f931a573a918", + "revCount": 216, + "type": "git", + "url": "ssh://git@github.com/flox/flox-bash" + }, + "original": { + "ref": "main", + "type": "git", + "url": "ssh://git@github.com/flox/flox-bash" + } + }, + "flox-floxpkgs": { + "inputs": { + "builtfilter": "builtfilter", + "capacitor": "capacitor", + "catalog": "catalog", + "flox": "flox", + "flox-bash": "flox-bash", + "nixpkgs": "nixpkgs_3", + "tracelinks": "tracelinks" + }, + "locked": { + "lastModified": 1680131458, + "narHash": "sha256-B0xvpF2h5iotKAMP13l0VtsXRAPwKQLitmxKdBSWLQQ=", + "owner": "flox", + "repo": "floxpkgs", + "rev": "6d8216d24ce8959b599107ccff8b15c959540b1a", + "type": "github" + }, + "original": { + "owner": "flox", + "repo": "floxpkgs", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "flox-floxpkgs", + "flox", + "shellHooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1660459072, + "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1676973346, + "narHash": "sha256-rft8oGMocTAhUVqG3LW6I8K/Fo9ICGmNjRqaWTJwav0=", + "owner": "flox", + "repo": "nixpkgs", + "rev": "d0d55259081f0b97c828f38559cad899d351cad1", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "stable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1679791877, + "narHash": "sha256-tTV1Mf0hPWIMtqyU16Kd2JUBDWvfHlDC9pF57vcbgpQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "cc060ddbf652a532b54057081d5abd6144d01971", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1679791877, + "narHash": "sha256-tTV1Mf0hPWIMtqyU16Kd2JUBDWvfHlDC9pF57vcbgpQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "cc060ddbf652a532b54057081d5abd6144d01971", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1678872516, + "narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable_2": { + "locked": { + "lastModified": 1676973346, + "narHash": "sha256-rft8oGMocTAhUVqG3LW6I8K/Fo9ICGmNjRqaWTJwav0=", + "owner": "flox", + "repo": "nixpkgs", + "rev": "d0d55259081f0b97c828f38559cad899d351cad1", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "stable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-staging": { + "locked": { + "lastModified": 1679262748, + "narHash": "sha256-DQCrrAFrkxijC6haUzOC5ZoFqpcv/tg2WxnyW3np1Cc=", + "owner": "flox", + "repo": "nixpkgs", + "rev": "60c1d71f2ba4c80178ec84523c2ca0801522e0a6", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "staging", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1679944645, + "narHash": "sha256-e5Qyoe11UZjVfgRfwNoSU57ZeKuEmjYb77B9IVW7L/M=", + "owner": "flox", + "repo": "nixpkgs", + "rev": "4bb072f0a8b267613c127684e099a70e1f6ff106", + "type": "github" + }, + "original": { + "owner": "flox", + "ref": "unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1678898370, + "narHash": "sha256-xTICr1j+uat5hk9FyuPOFGxpWHdJRibwZC+ATi0RbtE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac718d02867a84b42522a0ece52d841188208f2c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "inputs": { + "capacitor": "capacitor_2", + "flox": [ + "flox-floxpkgs", + "flox" + ], + "flox-bash": [ + "flox-floxpkgs", + "flox-bash" + ], + "flox-floxpkgs": [ + "flox-floxpkgs" + ], + "nixpkgs": [ + "flox-floxpkgs", + "nixpkgs", + "nixpkgs-stable" + ], + "nixpkgs-stable": "nixpkgs-stable_2", + "nixpkgs-staging": "nixpkgs-staging", + "nixpkgs-unstable": "nixpkgs-unstable", + "nixpkgs__flox__aarch64-darwin": "nixpkgs__flox__aarch64-darwin", + "nixpkgs__flox__aarch64-linux": "nixpkgs__flox__aarch64-linux", + "nixpkgs__flox__i686-linux": "nixpkgs__flox__i686-linux", + "nixpkgs__flox__x86_64-darwin": "nixpkgs__flox__x86_64-darwin", + "nixpkgs__flox__x86_64-linux": "nixpkgs__flox__x86_64-linux" + }, + "locked": { + "lastModified": 1680113891, + "narHash": "sha256-JiAmKV8ECf877cDbTum06NQ28n8FvwC4DYlCYzEVWxg=", + "owner": "flox", + "repo": "nixpkgs-flox", + "rev": "b7e7e40e2aa1ca2a44db440f5dc52213564af02f", + "type": "github" + }, + "original": { + "owner": "flox", + "repo": "nixpkgs-flox", + "type": "github" + } + }, + "nixpkgs__flox__aarch64-darwin": { + "flake": false, + "locked": { + "host": "catalog.floxsdlc.com", + "lastModified": 1680113678, + "narHash": "sha256-rCkwbly3gyvNcw21VBMXcf6EmF7crKZAS8Ylh2B5H+I=", + "owner": "flox", + "repo": "nixpkgs-flox", + "rev": "31d3b5fafdf504842e1e47a0c9b5888cf5dbae06", + "type": "github" + }, + "original": { + "host": "catalog.floxsdlc.com", + "owner": "flox", + "ref": "aarch64-darwin", + "repo": "nixpkgs-flox", + "type": "github" + } + }, + "nixpkgs__flox__aarch64-linux": { + "flake": false, + "locked": { + "host": "catalog.floxsdlc.com", + "lastModified": 1680113660, + "narHash": "sha256-ZofvvoatURsk1p5JvqajdrcO9iIT7UHjV2cb7io4FK0=", + "owner": "flox", + "repo": "nixpkgs-flox", + "rev": "8112f3ce4a227cd5088165d929761d877eea6709", + "type": "github" + }, + "original": { + "host": "catalog.floxsdlc.com", + "owner": "flox", + "ref": "aarch64-linux", + "repo": "nixpkgs-flox", + "type": "github" + } + }, + "nixpkgs__flox__i686-linux": { + "flake": false, + "locked": { + "host": "catalog.floxsdlc.com", + "lastModified": 1680113793, + "narHash": "sha256-xbjWq4lOP+K2772K8kDF17+CPoVyalqXYWyoH3x8au8=", + "owner": "flox", + "repo": "nixpkgs-flox", + "rev": "c189975540c856ac025be398761788c118ff5733", + "type": "github" + }, + "original": { + "host": "catalog.floxsdlc.com", + "owner": "flox", + "ref": "i686-linux", + "repo": "nixpkgs-flox", + "type": "github" + } + }, + "nixpkgs__flox__x86_64-darwin": { + "flake": false, + "locked": { + "host": "catalog.floxsdlc.com", + "lastModified": 1680113786, + "narHash": "sha256-HBNnTigb0MxCM6j6XmIH/0BRlUp7Rpe1et13TBf7xfM=", + "owner": "flox", + "repo": "nixpkgs-flox", + "rev": "5ab94a52855a10ad1ac0edd7277654bd587faf3e", + "type": "github" + }, + "original": { + "host": "catalog.floxsdlc.com", + "owner": "flox", + "ref": "x86_64-darwin", + "repo": "nixpkgs-flox", + "type": "github" + } + }, + "nixpkgs__flox__x86_64-linux": { + "flake": false, + "locked": { + "host": "catalog.floxsdlc.com", + "lastModified": 1680113768, + "narHash": "sha256-YnzWsglWn0TAeg2xQaVg8UFgALVU0TYZ/PdKV32EEtk=", + "owner": "flox", + "repo": "nixpkgs-flox", + "rev": "50ecffffa7c51a92a7a6bb9eb105625ed8b8f18d", + "type": "github" + }, + "original": { + "host": "catalog.floxsdlc.com", + "owner": "flox", + "ref": "x86_64-linux", + "repo": "nixpkgs-flox", + "type": "github" + } + }, + "root": { + "inputs": { + "flox-floxpkgs": "flox-floxpkgs" + } + }, + "shellHooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1678976941, + "narHash": "sha256-skNr08frCwN9NO+7I77MjOHHAw+L410/37JknNld+W4=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "32b1dbedfd77892a6e375737ef04d8efba634e9e", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "tracelinks": { + "inputs": { + "flox-floxpkgs": [ + "flox-floxpkgs" + ] + }, + "locked": { + "lastModified": 1674847293, + "narHash": "sha256-wPirp+8gIUpoAgE8zoXZalAJzCzcdDHKLEPOapJUtfs=", + "ref": "main", + "rev": "46108503f52bc2fcc948abb9b00fc65a13e5f5bd", + "revCount": 9, + "type": "git", + "url": "ssh://git@github.com/flox/tracelinks" + }, + "original": { + "ref": "main", + "type": "git", + "url": "ssh://git@github.com/flox/tracelinks" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..3fd85dbded --- /dev/null +++ b/flake.nix @@ -0,0 +1,7 @@ +{ + description = "A flox project"; + + inputs.flox-floxpkgs.url = "github:flox/floxpkgs"; + + outputs = args @ {flox-floxpkgs, ...}: flox-floxpkgs.project args (_: {}); +} diff --git a/flox.nix b/flox.nix new file mode 100644 index 0000000000..3f82311a7e --- /dev/null +++ b/flox.nix @@ -0,0 +1,3 @@ +{ + packages.nixpkgs-flox.go_1_20 = { }; +} diff --git a/go.mod b/go.mod index 6f7c4fe2c2..f434285b59 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/cloudflare/cloudflare-go -go 1.17 +go 1.18 require ( github.com/google/go-querystring v1.1.0 - github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/hashicorp/go-retryablehttp v0.7.2 github.com/olekukonko/tablewriter v0.0.5 - github.com/stretchr/testify v1.8.1 - github.com/urfave/cli/v2 v2.23.5 - golang.org/x/net v0.0.0-20220722155237-a158d28d115b - golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 + github.com/stretchr/testify v1.8.3 + github.com/urfave/cli/v2 v2.25.3 + golang.org/x/net v0.10.0 + golang.org/x/time v0.3.0 ) require gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect @@ -30,7 +30,6 @@ require ( github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 310708a4bf..36dbcedcf4 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -13,14 +11,13 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= -github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -53,42 +50,31 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= -github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= +github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 1dfd729e85..eb343b7063 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -222,10 +222,10 @@ require ( golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/tools v0.1.13-0.20220812184215-3f9b119300de // indirect golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df // indirect diff --git a/internal/tools/go.sum b/internal/tools/go.sum index f5841ad108..aeb2689f01 100644 --- a/internal/tools/go.sum +++ b/internal/tools/go.sum @@ -1005,8 +1005,8 @@ golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5o golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1103,10 +1103,10 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc= -golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1115,8 +1115,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/list.go b/list.go index 75e802fe1d..6cd9fe986b 100644 --- a/list.go +++ b/list.go @@ -14,6 +14,10 @@ const ( ListTypeIP = "ip" // ListTypeRedirect specifies a list containing redirects. ListTypeRedirect = "redirect" + // ListTypeHostname specifies a list containing hostnames. + ListTypeHostname = "hostname" + // ListTypeHostname specifies a list containing autonomous system numbers (ASNs). + ListTypeASN = "asn" ) // ListBulkOperation contains information about a Bulk Operation. @@ -47,11 +51,17 @@ type Redirect struct { PreservePathSuffix *bool `json:"preserve_path_suffix,omitempty"` } +type Hostname struct { + UrlHostname string `json:"url_hostname"` +} + // ListItem contains information about a single List Item. type ListItem struct { ID string `json:"id"` IP *string `json:"ip,omitempty"` Redirect *Redirect `json:"redirect,omitempty"` + Hostname *Hostname `json:"hostname,omitempty"` + ASN *uint32 `json:"asn,omitempty"` Comment string `json:"comment"` CreatedOn *time.Time `json:"created_on"` ModifiedOn *time.Time `json:"modified_on"` @@ -68,6 +78,8 @@ type ListCreateRequest struct { type ListItemCreateRequest struct { IP *string `json:"ip,omitempty"` Redirect *Redirect `json:"redirect,omitempty"` + Hostname *Hostname `json:"hostname,omitempty"` + ASN *uint32 `json:"asn,omitempty"` Comment string `json:"comment"` } diff --git a/list_test.go b/list_test.go index 9a73a0a8fd..0a622f8d1e 100644 --- a/list_test.go +++ b/list_test.go @@ -423,6 +423,117 @@ func TestListsItemsRedirect(t *testing.T) { } } +func TestListsItemsHostname(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": [ + { + "id": "0c0fc9fa937b11eaa1b71c4d701ab86e", + "hostname": { + "url_hostname": "cloudflare.com" + }, + "comment": "CF hostname", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + } + ], + "result_info": { + "cursors": { + "before": "xxx" + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := []ListItem{ + { + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + Hostname: &Hostname{ + UrlHostname: "cloudflare.com", + }, + Comment: "CF hostname", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + } + + actual, err := client.ListListItems( + context.Background(), + AccountIdentifier(testAccountID), + ListListItemsParams{ID: "0c0fc9fa937b11eaa1b71c4d701ab86e"}, + ) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListsItemsASN(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": [ + { + "id": "0c0fc9fa937b11eaa1b71c4d701ab86e", + "asn": 3456, + "comment": "ASN", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + } + ], + "result_info": { + "cursors": { + "before": "xxx" + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := []ListItem{ + { + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + ASN: Uint32Ptr(3456), + Comment: "ASN", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + } + + actual, err := client.ListListItems( + context.Background(), + AccountIdentifier(testAccountID), + ListListItemsParams{ID: "0c0fc9fa937b11eaa1b71c4d701ab86e"}, + ) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestCreateListItemsIP(t *testing.T) { setup() defer teardown() @@ -507,6 +618,93 @@ func TestCreateListItemsRedirect(t *testing.T) { } } +func TestCreateListItemsHostname(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.CreateListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListCreateItemsParams{ + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + Hostname: &Hostname{ + UrlHostname: "3fonteinen.be", // ie. only match 3fonteinen.be + }, + Comment: "hostname 3F", + }, { + Hostname: &Hostname{ + UrlHostname: "*.cf.com", // ie. match all subdomains of cf.com but not cf.com + }, + Comment: "Hostname cf", + }, { + Hostname: &Hostname{ + UrlHostname: "*.abc.com", // ie. equivalent to match all subdomains of abc.com excluding abc.com + }, + Comment: "Hostname abc", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateListItemsASN(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.CreateListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListCreateItemsParams{ + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + ASN: Uint32Ptr(458), + Comment: "ASN 458", + }, { + ASN: Uint32Ptr(789), + Comment: "ASN 789", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestReplaceListItemsIP(t *testing.T) { setup() defer teardown() @@ -591,6 +789,88 @@ func TestReplaceListItemsRedirect(t *testing.T) { } } +func TestReplaceListItemsHostname(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) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.ReplaceListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListReplaceItemsParams{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + Hostname: &Hostname{ + UrlHostname: "3fonteinen.be", + }, + Comment: "hostname 3F", + }, { + Hostname: &Hostname{ + UrlHostname: "cf.com", + }, + Comment: "Hostname cf", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestReplaceListItemsASN(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) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.ReplaceListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListReplaceItemsParams{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + ASN: Uint32Ptr(4567), + Comment: "ASN 4567", + }, { + ASN: Uint32Ptr(8901), + Comment: "ASN 8901", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestDeleteListItems(t *testing.T) { setup() defer teardown() @@ -667,6 +947,92 @@ func TestGetListItemIP(t *testing.T) { } } +func TestGetListItemHostname(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "hostname": { + "url_hostname": "cloudflare.com" + }, + "comment": "CF Hostname", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items/"+ + "34b12448945f11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := ListItem{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Hostname: &Hostname{ + UrlHostname: "cloudflare.com", + }, + Comment: "CF Hostname", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.GetListItem(context.Background(), AccountIdentifier(testAccountID), "2c0fc9fa937b11eaa1b71c4d701ab86e", "34b12448945f11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetListItemASN(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "asn": 5555, + "comment": "asn 5555", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items/"+ + "34b12448945f11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := ListItem{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + ASN: Uint32Ptr(5555), + Comment: "asn 5555", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.GetListItem(context.Background(), AccountIdentifier(testAccountID), "2c0fc9fa937b11eaa1b71c4d701ab86e", "34b12448945f11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestPollListTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 0) defer cancel() diff --git a/lockdown.go b/lockdown.go index 2a2f744647..a946e5a219 100644 --- a/lockdown.go +++ b/lockdown.go @@ -166,6 +166,7 @@ func (api *API) ListZoneLockdowns(ctx context.Context, rc *ResourceContainer, pa var zoneLockdowns []ZoneLockdown var zResponse ZoneLockdownListResponse for { + zResponse = ZoneLockdownListResponse{} uri := buildURI(fmt.Sprintf("/zones/%s/firewall/lockdowns", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/logpush.go b/logpush.go index fbb5354f11..8b3a70cb1a 100644 --- a/logpush.go +++ b/logpush.go @@ -11,19 +11,22 @@ import ( // LogpushJob describes a Logpush job. type LogpushJob struct { - ID int `json:"id,omitempty"` - Dataset string `json:"dataset"` - Enabled bool `json:"enabled"` - Kind string `json:"kind,omitempty"` - Name string `json:"name"` - LogpullOptions string `json:"logpull_options"` - DestinationConf string `json:"destination_conf"` - OwnershipChallenge string `json:"ownership_challenge,omitempty"` - LastComplete *time.Time `json:"last_complete,omitempty"` - LastError *time.Time `json:"last_error,omitempty"` - ErrorMessage string `json:"error_message,omitempty"` - Frequency string `json:"frequency,omitempty"` - Filter *LogpushJobFilters `json:"filter,omitempty"` + ID int `json:"id,omitempty"` + Dataset string `json:"dataset"` + Enabled bool `json:"enabled"` + Kind string `json:"kind,omitempty"` + Name string `json:"name"` + LogpullOptions string `json:"logpull_options"` + DestinationConf string `json:"destination_conf"` + OwnershipChallenge string `json:"ownership_challenge,omitempty"` + LastComplete *time.Time `json:"last_complete,omitempty"` + LastError *time.Time `json:"last_error,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + Frequency string `json:"frequency,omitempty"` + Filter *LogpushJobFilters `json:"filter,omitempty"` + MaxUploadBytes int `json:"max_upload_bytes,omitempty"` + MaxUploadRecords int `json:"max_upload_records,omitempty"` + MaxUploadIntervalSeconds int `json:"max_upload_interval_seconds,omitempty"` } type LogpushJobFilters struct { diff --git a/logpush_test.go b/logpush_test.go index dea1e82deb..c68e3b18b7 100644 --- a/logpush_test.go +++ b/logpush_test.go @@ -28,7 +28,8 @@ const ( "last_complete": "%[2]s", "last_error": "%[2]s", "error_message": "test", - "frequency": "high" + "frequency": "high", + "max_upload_bytes": 5000000 } ` serverEdgeLogpushJobDescription = `{ @@ -72,6 +73,7 @@ var ( LastError: &testLogpushTimestamp, ErrorMessage: "test", Frequency: "high", + MaxUploadBytes: 5000000, } expectedEdgeLogpushJobStruct = LogpushJob{ ID: jobID, @@ -177,18 +179,20 @@ func TestCreateLogpushJob(t *testing.T) { }{ "core logpush job": { newJob: LogpushJob{ - Dataset: "http_requests", - Enabled: false, - Name: "example.com", - LogpullOptions: "fields=RayID,ClientIP,EdgeStartTimestamp×tamps=rfc3339", - DestinationConf: "s3://mybucket/logs?region=us-west-2", + Dataset: "http_requests", + Enabled: false, + Name: "example.com", + LogpullOptions: "fields=RayID,ClientIP,EdgeStartTimestamp×tamps=rfc3339", + DestinationConf: "s3://mybucket/logs?region=us-west-2", + MaxUploadRecords: 1000, }, payload: `{ "dataset": "http_requests", "enabled":false, "name":"example.com", "logpull_options":"fields=RayID,ClientIP,EdgeStartTimestamp×tamps=rfc3339", - "destination_conf":"s3://mybucket/logs?region=us-west-2" + "destination_conf":"s3://mybucket/logs?region=us-west-2", + "max_upload_records": 1000 }`, result: serverLogpushJobDescription, want: expectedLogpushJobStruct, diff --git a/magic_transit_ipsec_tunnel.go b/magic_transit_ipsec_tunnel.go index cf9076a518..6ac5f22c2c 100644 --- a/magic_transit_ipsec_tunnel.go +++ b/magic_transit_ipsec_tunnel.go @@ -32,7 +32,7 @@ type MagicTransitIPsecTunnel struct { CreatedOn *time.Time `json:"created_on,omitempty"` ModifiedOn *time.Time `json:"modified_on,omitempty"` Name string `json:"name"` - CustomerEndpoint string `json:"customer_endpoint"` + CustomerEndpoint string `json:"customer_endpoint,omitempty"` CloudflareEndpoint string `json:"cloudflare_endpoint"` InterfaceAddress string `json:"interface_address"` Description string `json:"description,omitempty"` diff --git a/mtls_certificates.go b/mtls_certificates.go new file mode 100644 index 0000000000..5d92bd2dd5 --- /dev/null +++ b/mtls_certificates.go @@ -0,0 +1,214 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +// MTLSAssociation represents the metadata for an existing association +// between a user-uploaded mTLS certificate and a Cloudflare service. +type MTLSAssociation struct { + Service string `json:"service"` + Status string `json:"status"` +} + +// MTLSAssociationResponse represents the response from the retrieval endpoint +// for mTLS certificate associations. +type MTLSAssociationResponse struct { + Response + Result []MTLSAssociation `json:"result"` +} + +// MTLSCertificate represents the metadata for a user-uploaded mTLS +// certificate. +type MTLSCertificate struct { + ID string `json:"id"` + Name string `json:"name"` + Issuer string `json:"issuer"` + Signature string `json:"signature"` + SerialNumber string `json:"serial_number"` + Certificates string `json:"certificates"` + CA bool `json:"ca"` + UploadedOn time.Time `json:"uploaded_on"` + UpdatedAt time.Time `json:"updated_at"` + ExpiresOn time.Time `json:"expires_on"` +} + +// MTLSCertificateResponse represents the response from endpoints relating to +// retrieving, creating, and deleting an mTLS certificate. +type MTLSCertificateResponse struct { + Response + Result MTLSCertificate `json:"result"` +} + +// MTLSCertificatesResponse represents the response from the mTLS certificate +// list endpoint. +type MTLSCertificatesResponse struct { + Response + Result []MTLSCertificate `json:"result"` + ResultInfo `json:"result_info"` +} + +// MTLSCertificateParams represents the data related to the mTLS certificate +// being uploaded. Name is an optional field. +type CreateMTLSCertificateParams struct { + Name string `json:"name"` + Certificates string `json:"certificates"` + PrivateKey string `json:"private_key"` + CA bool `json:"ca"` +} + +type ListMTLSCertificatesParams struct { + PaginationOptions + Limit int `url:"limit,omitempty"` + Offset int `url:"offset,omitempty"` + Name string `url:"name,omitempty"` + CA bool `url:"ca,omitempty"` +} + +type ListMTLSCertificateAssociationsParams struct { + CertificateID string +} + +var ( + ErrMissingCertificateID = errors.New("missing required certificate ID") +) + +// ListMTLSCertificates returns a list of all user-uploaded mTLS certificates. +// +// API reference: https://api.cloudflare.com/#mtls-certificate-management-list-mtls-certificates +func (api *API) ListMTLSCertificates(ctx context.Context, rc *ResourceContainer, params ListMTLSCertificatesParams) ([]MTLSCertificate, ResultInfo, error) { + if rc.Level != AccountRouteLevel { + return []MTLSCertificate{}, ResultInfo{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return []MTLSCertificate{}, ResultInfo{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/mtls_certificates", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, params) + if err != nil { + return []MTLSCertificate{}, ResultInfo{}, err + } + var r MTLSCertificatesResponse + if err := json.Unmarshal(res, &r); err != nil { + return []MTLSCertificate{}, ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, r.ResultInfo, err +} + +// GetMTLSCertificate returns the metadata associated with a user-uploaded mTLS +// certificate. +// +// API reference: https://api.cloudflare.com/#mtls-certificate-management-get-mtls-certificate +func (api *API) GetMTLSCertificate(ctx context.Context, rc *ResourceContainer, certificateID string) (MTLSCertificate, error) { + if rc.Level != AccountRouteLevel { + return MTLSCertificate{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return MTLSCertificate{}, ErrMissingAccountID + } + + if certificateID == "" { + return MTLSCertificate{}, ErrMissingCertificateID + } + + uri := fmt.Sprintf("/accounts/%s/mtls_certificates/%s", rc.Identifier, certificateID) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return MTLSCertificate{}, err + } + var r MTLSCertificateResponse + if err := json.Unmarshal(res, &r); err != nil { + return MTLSCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +// ListMTLSCertificateAssociations returns a list of all existing associations +// between the mTLS certificate and Cloudflare services. +// +// API reference: https://api.cloudflare.com/#mtls-certificate-management-list-mtls-certificate-associations +func (api *API) ListMTLSCertificateAssociations(ctx context.Context, rc *ResourceContainer, params ListMTLSCertificateAssociationsParams) ([]MTLSAssociation, error) { + if rc.Level != AccountRouteLevel { + return []MTLSAssociation{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return []MTLSAssociation{}, ErrMissingAccountID + } + + if params.CertificateID == "" { + return []MTLSAssociation{}, ErrMissingCertificateID + } + + uri := fmt.Sprintf("/accounts/%s/mtls_certificates/%s/associations", rc.Identifier, params.CertificateID) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []MTLSAssociation{}, err + } + var r MTLSAssociationResponse + if err := json.Unmarshal(res, &r); err != nil { + return []MTLSAssociation{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +// CreateMTLSCertificate will create the provided certificate for use with mTLS +// enabled Cloudflare services. +// +// API reference: https://api.cloudflare.com/#mtls-certificate-management-upload-mtls-certificate +func (api *API) CreateMTLSCertificate(ctx context.Context, rc *ResourceContainer, params CreateMTLSCertificateParams) (MTLSCertificate, error) { + if rc.Level != AccountRouteLevel { + return MTLSCertificate{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return MTLSCertificate{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/mtls_certificates", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return MTLSCertificate{}, err + } + var r MTLSCertificateResponse + if err := json.Unmarshal(res, &r); err != nil { + return MTLSCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +// DeleteMTLSCertificate will delete the specified mTLS certificate. +// +// API reference: https://api.cloudflare.com/#mtls-certificate-management-delete-mtls-certificate +func (api *API) DeleteMTLSCertificate(ctx context.Context, rc *ResourceContainer, certificateID string) (MTLSCertificate, error) { + if rc.Level != AccountRouteLevel { + return MTLSCertificate{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return MTLSCertificate{}, ErrMissingAccountID + } + + if certificateID == "" { + return MTLSCertificate{}, ErrMissingCertificateID + } + + uri := fmt.Sprintf("/accounts/%s/mtls_certificates/%s", rc.Identifier, certificateID) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return MTLSCertificate{}, err + } + var r MTLSCertificateResponse + if err := json.Unmarshal(res, &r); err != nil { + return MTLSCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} diff --git a/mtls_certificates_test.go b/mtls_certificates_test.go new file mode 100644 index 0000000000..2652eae7b0 --- /dev/null +++ b/mtls_certificates_test.go @@ -0,0 +1,245 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGetMTLSCertificate(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + "name": "example_ca_cert_5", + "issuer": "O=Example Inc.,L=California,ST=San Francisco,C=US", + "signature": "SHA256WithRSA", + "serial_number": "235217144297995885180570755458463043449861756659", + "certificates": "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + "ca": true, + "uploaded_on": "2022-11-22T17:32:30.467938Z", + "expires_on": "2122-10-29T16:59:47Z" + } + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/mtls_certificates/2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", handler) + expiresOn, _ := time.Parse(time.RFC3339, "2122-10-29T16:59:47Z") + uploadedOn, _ := time.Parse(time.RFC3339, "2022-11-22T17:32:30.467938Z") + want := MTLSCertificate{ + ID: "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + Name: "example_ca_cert_5", + Issuer: "O=Example Inc.,L=California,ST=San Francisco,C=US", + Signature: "SHA256WithRSA", + SerialNumber: "235217144297995885180570755458463043449861756659", + Certificates: "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + CA: true, + UploadedOn: uploadedOn, + ExpiresOn: expiresOn, + } + + actual, err := client.GetMTLSCertificate(context.Background(), AccountIdentifier(testAccountID), "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListMTLSCertificates(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + "name": "example_ca_cert_5", + "issuer": "O=Example Inc.,L=California,ST=San Francisco,C=US", + "signature": "SHA256WithRSA", + "serial_number": "235217144297995885180570755458463043449861756659", + "certificates": "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + "ca": true, + "uploaded_on": "2022-11-22T17:32:30.467938Z", + "expires_on": "2122-10-29T16:59:47Z" + } + ], + "result_info": { + "page": 1, + "per_page": 50, + "count": 1, + "total_count": 1, + "total_pages": 1 + } + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/mtls_certificates", handler) + expiresOn, _ := time.Parse(time.RFC3339, "2122-10-29T16:59:47Z") + uploadedOn, _ := time.Parse(time.RFC3339, "2022-11-22T17:32:30.467938Z") + want := []MTLSCertificate{ + { + ID: "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + Name: "example_ca_cert_5", + Issuer: "O=Example Inc.,L=California,ST=San Francisco,C=US", + Signature: "SHA256WithRSA", + SerialNumber: "235217144297995885180570755458463043449861756659", + Certificates: "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + CA: true, + UploadedOn: uploadedOn, + ExpiresOn: expiresOn, + }, + } + + actual, _, err := client.ListMTLSCertificates(context.Background(), AccountIdentifier(testAccountID), ListMTLSCertificatesParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListCertificateAssociations(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "service": "gateway", + "status": "pending_deployment" + } + ] + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/mtls_certificates/2458ce5a-0c35-4c7f-82c7-8e9487d3ff60/associations", handler) + want := []MTLSAssociation{ + { + Service: "gateway", + Status: "pending_deployment", + }, + } + + actual, err := client.ListMTLSCertificateAssociations(context.Background(), AccountIdentifier(testAccountID), ListMTLSCertificateAssociationsParams{ + CertificateID: "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + }) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUploadMTLSCertificate(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + "name": "example_ca_cert_5", + "issuer": "O=Example Inc.,L=California,ST=San Francisco,C=US", + "signature": "SHA256WithRSA", + "serial_number": "235217144297995885180570755458463043449861756659", + "certificates": "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + "ca": true, + "uploaded_on": "2022-11-22T17:32:30.467938Z", + "updated_at": "2022-11-22T17:32:30.467938Z", + "expires_on": "2122-10-29T16:59:47Z" + } + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/mtls_certificates", handler) + expiresOn, _ := time.Parse(time.RFC3339, "2122-10-29T16:59:47Z") + uploadedOn, _ := time.Parse(time.RFC3339, "2022-11-22T17:32:30.467938Z") + want := MTLSCertificate{ + ID: "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + Name: "example_ca_cert_5", + Issuer: "O=Example Inc.,L=California,ST=San Francisco,C=US", + Signature: "SHA256WithRSA", + SerialNumber: "235217144297995885180570755458463043449861756659", + Certificates: "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + CA: true, + UploadedOn: uploadedOn, + UpdatedAt: uploadedOn, + ExpiresOn: expiresOn, + } + + cert := CreateMTLSCertificateParams{ + Name: "example_ca_cert_5", + Certificates: "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + PrivateKey: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEXDkcICRU3XBv9hiiPnBWIjgTQyowmVFxDr11mONgZB/cMYjE/OvQjvnpwNcOaSK16MOpAjNbELKRx2lZiVJaLRDCccqCxXwP/CrdRChcqGzo7mbNksMlcidrErb0LlEBKLFC2QjRmRKqB+YOs4TD8WsZu2S667A2fZmjRlaqOxFi1h62ee0P+TLU628UC/nl41JifSt5Evt7hMDHakemdwZblNYr2p6T3NQjdhjYXTtP4UmOGJBhJ7i7Kicg3d3CIgdTMbggSeGWqjndr4ldVnD96FN3cVT5uDFsn2CJXTFgdeBWoUnMS4VnUZzPWGf4vSBXC8qV7Ls+w46yT7T1AgMBAAECggEAQZnp/oqCeNPOR6l5S2L+1tfx0gWjZ78hJVteUpZ0iHSK7F6kKeOxyOird7vUXV0kmo+cJq+0hp0Ke4eam640FCpwKfYoSQ4/R3vgujGWJnaihCN5tv5sMet0XeJPuz5qE7ALoKCvwI6aXLHs20aAeZIDTQJ9QbGSGnJVzOWn+JDTidIgZpN57RpXfSAwnJPTQK/PN8i5z108hsaDOdEgGmxYZ7kYqMqzX20KXmth58LDfPixs5JGtS60iiKC/wOcGzkB2/AdTSojR76oEU77cANP/3zO25NG//whUdYlW0t0d7PgXxIeJe+xgYnamDQJx3qonVyt4H77ha0ObRAj9QKBgQDicZr+VTwFMnELP3a+FXGnjehRiuS1i7MXGKxNweCD+dFlML0FplSQS8Ro2n+d8lu8BBXGx0qm6VXu8Rhn7TAUL6q+PCgfarzxfIhacb/TZCqfieIHsMlVBfhV5HCXnk+kis0tuC/PRArcWTwDHJUJXkBhvkUsNswvQzavDPI7KwKBgQDd/WgLkj7A3X5fgIHZH/GbDSBiXwzKb+rF4ZCT2XFgG/OAW7vapfcX/w+v+5lBLyrocmOAS3PGGAhM5T3HLnUCQfnK4qgps1Lqibkc9Tmnsn60LanUjuUMsYv/zSw70tozbzhJ0pioEpWfRxRZBztO2Rr8Ntm7h6Fk701EXGNAXwKBgQCD1xsjy2J3sCerIdcz0u5qXLAPkeuZW+34m4/ucdwTWwc0gEz9lhsULFj9p4G351zLuiEnq+7mAWLcDJlmIO3mQt6JhiLiL9Y0T4pgBmxmWqKKYtAsJB0EmMY+1BNN44mBRqMxZFTJu1cLdhT/xstrOeoIPqytknYNanfTMZlzIwKBgHrLXe5oq0XMP8dcMneEcAUwsaU4pr6kQd3L9EmUkl5zl7J9C+DaxWAEuwzBw/iGutlxzRB+rD/7szu14wJ29EqXbDGKRzMp+se5/yfBjm7xEZ1hVPw7PwBShfqt57X/4Ktq7lwHnmH6RcGhc+P7WBc5iO/S94YAdIp8xOT3pf9JAoGAE0QkqJUY+5Mgr+fBO0VNV72ZoPveGpW+De59uhKAOnu1zljQCUtk59m6+DXfm0tNYKtawa5n8iN71Zh+s62xXSt3pYi1Y5CCCmv8Y4BhwIcPwXKk3zEvLgSHVTpC0bayA9aSO4bbZgVXa5w+Z0w/vvfp9DWo1IS3EnQRrz6WMYA=\n-----END PRIVATE KEY-----", + CA: true, + } + actual, err := client.CreateMTLSCertificate(context.Background(), AccountIdentifier(testAccountID), cert) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteMTLSCertificate(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + "name": "example_ca_cert_5", + "issuer": "O=Example Inc.,L=California,ST=San Francisco,C=US", + "signature": "SHA256WithRSA", + "serial_number": "235217144297995885180570755458463043449861756659", + "certificates": "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + "ca": true, + "uploaded_on": "2022-11-22T17:32:30.467938Z", + "expires_on": "2122-10-29T16:59:47Z" + } + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/mtls_certificates/2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", handler) + expiresOn, _ := time.Parse(time.RFC3339, "2122-10-29T16:59:47Z") + uploadedOn, _ := time.Parse(time.RFC3339, "2022-11-22T17:32:30.467938Z") + want := MTLSCertificate{ + ID: "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60", + Name: "example_ca_cert_5", + Issuer: "O=Example Inc.,L=California,ST=San Francisco,C=US", + Signature: "SHA256WithRSA", + SerialNumber: "235217144297995885180570755458463043449861756659", + Certificates: "-----BEGIN CERTIFICATE-----\nMIIDmDCCAoCgAwIBAgIUKTOAZNjcXVZRj4oQt0SHsl1c1vMwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjAgFw0yMjExMjIxNjU5NDdaGA8yMTIyMTAyOTE2NTk0N1owUTELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNhbGlmb3JuaWExFTATBgNVBAoMDEV4YW1wbGUgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRcORwgJFTdcG/2GKI+cFYiOBNDKjCZUXEOvXWY42BkH9wxiMT869CO+enA1w5pIrXow6kCM1sQspHHaVmJUlotEMJxyoLFfA/8Kt1EKFyobOjuZs2SwyVyJ2sStvQuUQEosULZCNGZEqoH5g6zhMPxaxm7ZLrrsDZ9maNGVqo7EWLWHrZ57Q/5MtTrbxQL+eXjUmJ9K3kS+3uEwMdqR6Z3BluU1ivanpPc1CN2GNhdO0/hSY4YkGEnuLsqJyDd3cIiB1MxuCBJ4ZaqOd2viV1WcP3oU3dxVPm4MWyfYIldMWB14FahScxLhWdRnM9YZ/i9IFcLypXsuz7DjrJPtPUCAwEAAaNmMGQwHQYDVR0OBBYEFP5JzLUawNF+c3AXsYTEWHh7z2czMB8GA1UdIwQYMBaAFP5JzLUawNF+c3AXsYTEWHh7z2czMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4IBAQBc+Be7NDhpE09y7hLPZGRPl1cSKBw4RI0XIv6rlbSTFs5EebpTGjhx/whNxwEZhB9HZ7111Oa1YlT8xkI9DshB78mjAHCKBAJ76moK8tkG0aqdYpJ4ZcJTVBB7l98Rvgc7zfTii7WemTy72deBbSeiEtXavm4EF0mWjHhQ5Nxpnp00Bqn5g1x8CyTDypgmugnep+xG+iFzNmTdsz7WI9T/7kDMXqB7M/FPWBORyS98OJqNDswCLF8bIZYwUBEe+bRHFomoShMzaC3tvim7WCb16noDkSTMlfKO4pnvKhpcVdSgwcruATV7y+W+Lvmz2OT/Gui4JhqeoTewsxndhDDE\n-----END CERTIFICATE-----", + CA: true, + UploadedOn: uploadedOn, + ExpiresOn: expiresOn, + } + + actual, err := client.DeleteMTLSCertificate(context.Background(), AccountIdentifier(testAccountID), "2458ce5a-0c35-4c7f-82c7-8e9487d3ff60") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} diff --git a/origin_ca.go b/origin_ca.go index a7eaea7240..45c8951570 100644 --- a/origin_ca.go +++ b/origin_ca.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/http" - "net/url" "time" ) @@ -25,6 +24,17 @@ type OriginCACertificate struct { CSR string `json:"csr"` } +type CreateOriginCertificateParams struct { + ID string `json:"id"` + Certificate string `json:"certificate"` + Hostnames []string `json:"hostnames"` + ExpiresOn time.Time `json:"expires_on"` + RequestType string `json:"request_type"` + RequestValidity int `json:"requested_validity"` + RevokedAt time.Time `json:"revoked_at,omitempty"` + CSR string `json:"csr"` +} + // UnmarshalJSON handles custom parsing from an API response to an OriginCACertificate // http://choly.ca/post/go-json-marshalling/ func (c *OriginCACertificate) UnmarshalJSON(data []byte) error { @@ -57,9 +67,10 @@ func (c *OriginCACertificate) UnmarshalJSON(data []byte) error { return nil } -// OriginCACertificateListOptions represents the parameters used to list Cloudflare-issued certificates. -type OriginCACertificateListOptions struct { - ZoneID string +// ListOriginCertificatesParams represents the parameters used to list +// Cloudflare-issued certificates. +type ListOriginCertificatesParams struct { + ZoneID string `url:"zone_id,omitempty"` } // OriginCACertificateID represents the ID of the revoked certificate from the Revoke Certificate endpoint. @@ -86,17 +97,13 @@ type originCACertificateResponseRevoke struct { Result OriginCACertificateID `json:"result"` } -// CreateOriginCertificate creates a Cloudflare-signed certificate. -// -// This function requires api.APIUserServiceKey be set to your Certificates API key. +// CreateOriginCACertificate creates a Cloudflare-signed certificate. // // API reference: https://api.cloudflare.com/#cloudflare-ca-create-certificate -func (api *API) CreateOriginCertificate(ctx context.Context, certificate OriginCACertificate) (*OriginCACertificate, error) { - uri := "/certificates" - res, err := api.makeRequestWithAuthType(ctx, http.MethodPost, uri, certificate, AuthUserService) - +func (api *API) CreateOriginCACertificate(ctx context.Context, params CreateOriginCertificateParams) (*OriginCACertificate, error) { + res, err := api.makeRequestContext(ctx, http.MethodPost, "/certificates", params) if err != nil { - return nil, err + return &OriginCACertificate{}, err } var originResponse *originCACertificateResponse @@ -114,18 +121,12 @@ func (api *API) CreateOriginCertificate(ctx context.Context, certificate OriginC return &originResponse.Result, nil } -// OriginCertificates lists all Cloudflare-issued certificates. -// -// This function requires api.APIUserServiceKey be set to your Certificates API key. +// ListOriginCACertificates lists all Cloudflare-issued certificates. // // API reference: https://api.cloudflare.com/#cloudflare-ca-list-certificates -func (api *API) OriginCertificates(ctx context.Context, options OriginCACertificateListOptions) ([]OriginCACertificate, error) { - v := url.Values{} - if options.ZoneID != "" { - v.Set("zone_id", options.ZoneID) - } - uri := fmt.Sprintf("/certificates?%s", v.Encode()) - res, err := api.makeRequestWithAuthType(ctx, http.MethodGet, uri, nil, AuthUserService) +func (api *API) ListOriginCACertificates(ctx context.Context, params ListOriginCertificatesParams) ([]OriginCACertificate, error) { + uri := buildURI("/certificates", params) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return nil, err @@ -146,14 +147,13 @@ func (api *API) OriginCertificates(ctx context.Context, options OriginCACertific return originResponse.Result, nil } -// OriginCertificate returns the details for a Cloudflare-issued certificate. -// -// This function requires api.APIUserServiceKey be set to your Certificates API key. +// GetOriginCACertificate returns the details for a Cloudflare-issued +// certificate. // // API reference: https://api.cloudflare.com/#cloudflare-ca-certificate-details -func (api *API) OriginCertificate(ctx context.Context, certificateID string) (*OriginCACertificate, error) { +func (api *API) GetOriginCACertificate(ctx context.Context, certificateID string) (*OriginCACertificate, error) { uri := fmt.Sprintf("/certificates/%s", certificateID) - res, err := api.makeRequestWithAuthType(ctx, http.MethodGet, uri, nil, AuthUserService) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return nil, err @@ -174,14 +174,12 @@ func (api *API) OriginCertificate(ctx context.Context, certificateID string) (*O return &originResponse.Result, nil } -// RevokeOriginCertificate revokes a created certificate for a zone. -// -// This function requires api.APIUserServiceKey be set to your Certificates API key. +// RevokeOriginCACertificate revokes a created certificate for a zone. // // API reference: https://api.cloudflare.com/#cloudflare-ca-revoke-certificate -func (api *API) RevokeOriginCertificate(ctx context.Context, certificateID string) (*OriginCACertificateID, error) { +func (api *API) RevokeOriginCACertificate(ctx context.Context, certificateID string) (*OriginCACertificateID, error) { uri := fmt.Sprintf("/certificates/%s", certificateID) - res, err := api.makeRequestWithAuthType(ctx, http.MethodDelete, uri, nil, AuthUserService) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) if err != nil { return nil, err @@ -204,7 +202,7 @@ func (api *API) RevokeOriginCertificate(ctx context.Context, certificateID strin // Gets the Cloudflare Origin CA Root Certificate for a given algorithm in PEM format. // Algorithm must be one of ['ecc', 'rsa']. -func OriginCARootCertificate(algorithm string) ([]byte, error) { +func GetOriginCARootCertificate(algorithm string) ([]byte, error) { var url string switch algorithm { case "ecc": diff --git a/origin_ca_test.go b/origin_ca_test.go index 00510804ac..855472b9c5 100644 --- a/origin_ca_test.go +++ b/origin_ca_test.go @@ -74,7 +74,7 @@ func TestOriginCA_CreateOriginCertificate(t *testing.T) { expiresOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") - testCertificate := OriginCACertificate{ + testCertificate := CreateOriginCertificateParams{ ID: "0x47530d8f561faa08", Certificate: "-----BEGIN CERTIFICATE-----MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNVBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGlnaUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmowp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiIWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPRBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6DhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpYQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO297Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=-----END CERTIFICATE-----", Hostnames: []string{"example.com", "*.another.com"}, @@ -84,10 +84,18 @@ func TestOriginCA_CreateOriginCertificate(t *testing.T) { CSR: "-----BEGIN CERTIFICATE REQUEST-----MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNVBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGlnaUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmowp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiIWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPRBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6DhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpYQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO297Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=-----END CERTIFICATE REQUEST-----", } - createdCertificate, err := client.CreateOriginCertificate(context.Background(), testCertificate) + createdCertificate, err := client.CreateOriginCACertificate(context.Background(), testCertificate) if assert.NoError(t, err) { - assert.Equal(t, createdCertificate, &testCertificate) + assert.Equal(t, &OriginCACertificate{ + ID: "0x47530d8f561faa08", + Certificate: "-----BEGIN CERTIFICATE-----MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNVBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGlnaUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmowp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiIWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPRBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6DhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpYQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO297Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=-----END CERTIFICATE-----", + Hostnames: []string{"example.com", "*.another.com"}, + ExpiresOn: expiresOn, + RequestType: "origin-rsa", + RequestValidity: 5475, + CSR: "-----BEGIN CERTIFICATE REQUEST-----MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNVBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGlnaUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmowp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiIWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPRBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6DhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpYQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO297Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=-----END CERTIFICATE REQUEST-----", + }, createdCertificate) } } @@ -138,7 +146,7 @@ func TestOriginCA_OriginCertificates(t *testing.T) { CSR: "-----BEGIN CERTIFICATE REQUEST-----MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNVBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGlnaUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmowp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiIWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPRBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6DhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpYQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO297Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=-----END CERTIFICATE REQUEST-----", } - certs, err := client.OriginCertificates(context.Background(), OriginCACertificateListOptions{ZoneID: testZoneID}) + certs, err := client.ListOriginCACertificates(context.Background(), ListOriginCertificatesParams{ZoneID: testZoneID}) if assert.NoError(t, err) { assert.IsType(t, []OriginCACertificate{}, certs, "Expected type []OriginCACertificate and got %v", certs) @@ -187,7 +195,7 @@ func TestOriginCA_OriginCertificate(t *testing.T) { CSR: "-----BEGIN CERTIFICATE REQUEST-----MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNVBAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGlnaUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmowp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiIWDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZwIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPRBPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6DhJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpYQ4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO297Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=-----END CERTIFICATE REQUEST-----", } - cert, err := client.OriginCertificate(context.Background(), testCertificate.ID) + cert, err := client.GetOriginCACertificate(context.Background(), testCertificate.ID) if assert.NoError(t, err) { assert.IsType(t, &OriginCACertificate{}, cert, "Expected type &OriginCACertificate and got %v", cert) @@ -216,7 +224,7 @@ func TestOriginCA_RevokeCertificate(t *testing.T) { ID: "0x47530d8f561faa08", } - cert, err := client.RevokeOriginCertificate(context.Background(), testCertificate.ID) + cert, err := client.RevokeOriginCACertificate(context.Background(), testCertificate.ID) if assert.NoError(t, err) { assert.IsType(t, &OriginCACertificateID{}, cert, "Expected type &OriginCACertificateID and got %v", cert) @@ -237,7 +245,7 @@ func TestOriginCA_OriginCARootCertificate(t *testing.T) { for _, algorithm := range algorithms { t.Logf("get origin CA root certificate for algorithm %s", algorithm) - rootCACert, err := OriginCARootCertificate(algorithm) + rootCACert, err := GetOriginCARootCertificate(algorithm) if assert.NoError(t, err) { assert.NotNil(t, rootCACert) diff --git a/pages_deployment_test.go b/pages_deployment_test.go index 9696d74382..282710a0cf 100644 --- a/pages_deployment_test.go +++ b/pages_deployment_test.go @@ -91,54 +91,6 @@ const ( "aliases": null }` - testPagesDeploymentStageLogsResponse = ` - { - "name": "build", - "started_on": "2021-01-01T00:00:00Z", - "ended_on": "2021-01-01T00:00:00Z", - "status": "success", - "start": 0, - "end": 5, - "total": 6, - "data": [ - { - "id": 0, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Installing dependencies" - }, - { - "id": 1, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Verify run directory" - }, - { - "id": 2, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Executing user command: bash test.sh" - }, - { - "id": 3, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Finished" - }, - { - "id": 4, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Building functions..." - }, - { - "id": 5, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Validating asset output directory" - }, - { - "id": 6, - "timestamp": "2021-01-01T00:00:00Z", - "message": "Parsed 2 valid header rules." - } - ] - }` - testPagesDeploymentLogsResponse = ` { "total": 6, @@ -190,9 +142,9 @@ var ( ModifiedOn: &pagesDeploymentDummyTime, Aliases: nil, LatestStage: *expectedPagesDeploymentLatestStage, - EnvVars: map[string]map[string]string{ - "NODE_VERSION": { - "value": "16", + EnvVars: EnvironmentVariableMap{ + "NODE_VERSION": &EnvironmentVariable{ + Value: "16", }, }, DeploymentTrigger: PagesProjectDeploymentTrigger{ @@ -255,17 +207,6 @@ var ( Status: "success", } - expectedPagesDeploymentStageLogs = &PagesDeploymentStageLogs{ - Name: "build", - StartedOn: &pagesDeploymentDummyTime, - EndedOn: &pagesDeploymentDummyTime, - Status: "success", - Start: 0, - End: 5, - Total: 6, - Data: expectedPagesDeploymentStageLogEntries, - } - expectedPagesDeploymentStageLogEntries = []PagesDeploymentStageLogEntry{ { ID: 0, diff --git a/pages_project.go b/pages_project.go index 753c1eb8d8..4cbbb46497 100644 --- a/pages_project.go +++ b/pages_project.go @@ -67,18 +67,18 @@ type PagesProjectDeploymentConfigs struct { // PagesProjectDeploymentConfigEnvironment represents the configuration for preview or production deploys. type PagesProjectDeploymentConfigEnvironment struct { - EnvVars map[string]PagesProjectDeploymentVar `json:"env_vars"` - CompatibilityDate string `json:"compatibility_date,omitempty"` - CompatibilityFlags []string `json:"compatibility_flags,omitempty"` - KvNamespaces NamespaceBindingMap `json:"kv_namespaces,omitempty"` - DoNamespaces NamespaceBindingMap `json:"durable_object_namespaces,omitempty"` - D1Databases D1BindingMap `json:"d1_databases,omitempty"` - R2Bindings R2BindingMap `json:"r2_buckets,omitempty"` -} - -// PagesProjectDeploymentVar represents a deployment environment variable. -type PagesProjectDeploymentVar struct { - Value string `json:"value"` + EnvVars EnvironmentVariableMap `json:"env_vars,omitempty"` + KvNamespaces NamespaceBindingMap `json:"kv_namespaces,omitempty"` + DoNamespaces NamespaceBindingMap `json:"durable_object_namespaces,omitempty"` + D1Databases D1BindingMap `json:"d1_databases,omitempty"` + R2Bindings R2BindingMap `json:"r2_buckets,omitempty"` + ServiceBindings ServiceBindingMap `json:"services,omitempty"` + CompatibilityDate string `json:"compatibility_date,omitempty"` + CompatibilityFlags []string `json:"compatibility_flags,omitempty"` + FailOpen bool `json:"fail_open"` + AlwaysUseLatestCompatibilityDate bool `json:"always_use_latest_compatibility_date"` + UsageModel UsageModel `json:"usage_model,omitempty"` + Placement *Placement `json:"placement,omitempty"` } // PagesProjectDeployment represents a deployment to a Pages project. @@ -93,13 +93,21 @@ type PagesProjectDeployment struct { ModifiedOn *time.Time `json:"modified_on"` Aliases []string `json:"aliases,omitempty"` LatestStage PagesProjectDeploymentStage `json:"latest_stage"` - EnvVars map[string]map[string]string `json:"env_vars"` + EnvVars EnvironmentVariableMap `json:"env_vars"` + KvNamespaces NamespaceBindingMap `json:"kv_namespaces,omitempty"` + DoNamespaces NamespaceBindingMap `json:"durable_object_namespaces,omitempty"` + D1Databases D1BindingMap `json:"d1_databases,omitempty"` + R2Bindings R2BindingMap `json:"r2_buckets,omitempty"` + ServiceBindings ServiceBindingMap `json:"services,omitempty"` + Placement *Placement `json:"placement,omitempty"` DeploymentTrigger PagesProjectDeploymentTrigger `json:"deployment_trigger"` Stages []PagesProjectDeploymentStage `json:"stages"` BuildConfig PagesProjectBuildConfig `json:"build_config"` Source PagesProjectSource `json:"source"` CompatibilityDate string `json:"compatibility_date,omitempty"` CompatibilityFlags []string `json:"compatibility_flags,omitempty"` + UsageModel UsageModel `json:"usage_model,omitempty"` + IsSkipped bool `json:"is_skipped"` ProductionBranch string `json:"production_branch,omitempty"` } @@ -135,6 +143,21 @@ type pagesProjectListResponse struct { ResultInfo `json:"result_info"` } +type EnvironmentVariableMap map[string]*EnvironmentVariable + +// PagesProjectDeploymentVar represents a deployment environment variable. +type EnvironmentVariable struct { + Value string `json:"value"` + Type EnvVarType `json:"type"` +} + +type EnvVarType string + +const ( + PlainText EnvVarType = "plain_text" + SecretText EnvVarType = "secret_text" +) + type NamespaceBindingMap map[string]*NamespaceBindingValue type NamespaceBindingValue struct { @@ -153,6 +176,20 @@ type D1Binding struct { ID string `json:"id"` } +type ServiceBindingMap map[string]*ServiceBinding + +type ServiceBinding struct { + Service string `json:"service"` + Environment string `json:"environment"` +} + +type UsageModel string + +const ( + Bundled UsageModel = "bundled" + Unbound UsageModel = "unbound" +) + // ListPagesProjects returns all Pages projects for an account. // // API reference: https://api.cloudflare.com/#pages-project-get-projects diff --git a/pages_project_test.go b/pages_project_test.go index 0dffd3c30a..32d09ab570 100644 --- a/pages_project_test.go +++ b/pages_project_test.go @@ -58,7 +58,11 @@ const ( }, "ENV": { "value": "preview" - } + }, + "API_KEY": { + "value": "", + "type": "secret_text" + } }, "compatibility_date": "2022-08-15", "compatibility_flags": ["preview_flag"] @@ -70,7 +74,11 @@ const ( }, "ENV": { "value": "production" - } + }, + "API_KEY": { + "value": "", + "type": "secret_text" + } }, "d1_databases": { "D1_BINDING": { @@ -92,8 +100,20 @@ const ( "name": "some-bucket" } }, + "services": { + "SERVICE_BINDING": { + "service": "some-worker", + "environment": "production" + } + }, "compatibility_date": "2022-08-15", - "compatibility_flags": ["production_flag"] + "compatibility_flags": ["production_flag"], + "fail_open": false, + "always_use_latest_compatibility_date": false, + "usage_model": "bundled", + "placement": { + "mode": "smart" + } } }, "latest_deployment": { @@ -121,10 +141,20 @@ const ( }, "ENV": { "value": "STAGING" - } + }, + "API_KEY": { + "value": "", + "type": "secret_text" + } }, + "placement": { + "mode": "smart" + }, "compatibility_date": "2022-08-15", "compatibility_flags": ["deployment_flag"], + "fail_open": false, + "always_use_latest_compatibility_date": false, + "usage_model": "bundled", "deployment_trigger": { "type": "ad_hoc", "metadata": { @@ -201,10 +231,21 @@ const ( }, "ENV": { "value": "STAGING" - } + }, + "API_KEY": { + "value": "", + "type": "secret_text" + } }, + "placement": { + "mode": "smart" + }, "compatibility_date": "2022-08-15", "compatibility_flags": ["deployment_flag"], + "fail_open": false, + "always_use_latest_compatibility_date": false, + "build_image_major_version": 1, + "usage_model": "bundled", "deployment_trigger": { "type": "ad_hoc", "metadata": { @@ -295,16 +336,24 @@ var ( "https://branchname.pages-test.pages.dev", }, LatestStage: *expectedPagesProjectLatestDeploymentStage, - EnvVars: map[string]map[string]string{ - "BUILD_VERSION": { - "value": "1.2", + EnvVars: EnvironmentVariableMap{ + "BUILD_VERSION": &EnvironmentVariable{ + Value: "1.2", }, - "ENV": { - "value": "STAGING", + "ENV": &EnvironmentVariable{ + Value: "STAGING", }, + "API_KEY": &EnvironmentVariable{ + Value: "", + Type: SecretText, + }, + }, + Placement: &Placement{ + Mode: PlacementModeSmart, }, CompatibilityFlags: []string{"deployment_flag"}, CompatibilityDate: "2022-08-15", + UsageModel: Bundled, DeploymentTrigger: *expectedPagesProjectDeploymentTrigger, Stages: expectedStages, BuildConfig: *expectedPagesProjectBuildConfig, @@ -363,29 +412,35 @@ var ( } expectedPagesProjectDeploymentConfigPreview = &PagesProjectDeploymentConfigEnvironment{ - EnvVars: map[string]PagesProjectDeploymentVar{ - "BUILD_VERSION": { + EnvVars: EnvironmentVariableMap{ + "BUILD_VERSION": &EnvironmentVariable{ Value: "1.2", }, - "ENV": { + "ENV": &EnvironmentVariable{ Value: "preview", }, + "API_KEY": &EnvironmentVariable{ + Value: "", + Type: SecretText, + }, }, CompatibilityDate: "2022-08-15", CompatibilityFlags: []string{"preview_flag"}, } expectedPagesProjectDeploymentConfigProduction = &PagesProjectDeploymentConfigEnvironment{ - EnvVars: map[string]PagesProjectDeploymentVar{ - "BUILD_VERSION": { + EnvVars: EnvironmentVariableMap{ + "BUILD_VERSION": &EnvironmentVariable{ Value: "1.2", }, - "ENV": { + "ENV": &EnvironmentVariable{ Value: "production", }, + "API_KEY": &EnvironmentVariable{ + Value: "", + Type: SecretText, + }, }, - CompatibilityDate: "2022-08-15", - CompatibilityFlags: []string{"production_flag"}, KvNamespaces: NamespaceBindingMap{ "KV_BINDING": &NamespaceBindingValue{Value: "5eb63bbbe01eeed093cb22bb8f5acdc3"}, }, @@ -398,6 +453,20 @@ var ( R2Bindings: R2BindingMap{ "R2_BINDING": &R2BindingValue{Name: "some-bucket"}, }, + ServiceBindings: ServiceBindingMap{ + "SERVICE_BINDING": &ServiceBinding{ + Service: "some-worker", + Environment: "production", + }, + }, + CompatibilityDate: "2022-08-15", + CompatibilityFlags: []string{"production_flag"}, + FailOpen: false, + AlwaysUseLatestCompatibilityDate: false, + UsageModel: Bundled, + Placement: &Placement{ + Mode: PlacementModeSmart, + }, } expectedPagesProjectSource = &PagesProjectSource{ diff --git a/queue.go b/queue.go index 0bd5a4dfa6..771a1f1482 100644 --- a/queue.go +++ b/queue.go @@ -78,14 +78,8 @@ type QueueConsumerResponse struct { } type UpdateQueueParams struct { - ID string `json:"queue_id,omitempty"` - Name string `json:"queue_name,omitempty"` - CreatedOn *time.Time `json:"created_on,omitempty"` - ModifiedOn *time.Time `json:"modified_on,omitempty"` - ProducersTotalCount int `json:"producers_total_count,omitempty"` - Producers []QueueProducer `json:"producers,omitempty"` - ConsumersTotalCount int `json:"consumers_total_count,omitempty"` - Consumers []QueueConsumer `json:"consumers,omitempty"` + Name string `json:"-"` + UpdatedName string `json:"queue_name,omitempty"` } type ListQueueConsumersParams struct { @@ -129,6 +123,7 @@ func (api *API) ListQueues(ctx context.Context, rc *ResourceContainer, params Li var queues []Queue var qResponse QueueListResponse for { + qResponse = QueueListResponse{} uri := buildURI(fmt.Sprintf("/accounts/%s/workers/queues", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) @@ -232,12 +227,12 @@ func (api *API) UpdateQueue(ctx context.Context, rc *ResourceContainer, params U return Queue{}, ErrMissingAccountID } - if params.Name == "" { + if params.Name == "" || params.UpdatedName == "" { return Queue{}, ErrMissingQueueName } uri := fmt.Sprintf("/accounts/%s/workers/queues/%s", rc.Identifier, params.Name) - res, err := api.makeRequestContext(ctx, http.MethodPut, uri, nil) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) if err != nil { return Queue{}, fmt.Errorf("%s: %w", errMakeRequestError, err) } @@ -276,6 +271,7 @@ func (api *API) ListQueueConsumers(ctx context.Context, rc *ResourceContainer, p var queuesConsumers []QueueConsumer var qResponse ListQueueConsumersResponse for { + qResponse = ListQueueConsumersResponse{} uri := buildURI(fmt.Sprintf("/accounts/%s/workers/queues/%s/consumers", rc.Identifier, params.QueueName), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) diff --git a/queue_test.go b/queue_test.go index 5485b354ca..b175a7045d 100644 --- a/queue_test.go +++ b/queue_test.go @@ -272,17 +272,17 @@ func TestQueue_Update(t *testing.T) { } }`) }) - _, err := client.UpdateQueue(context.Background(), AccountIdentifier(""), UpdateQueueParams{}) + _, err := client.UpdateQueue(context.Background(), AccountIdentifier(""), UpdateQueueParams{Name: testQueueName}) if assert.Error(t, err) { assert.Equal(t, ErrMissingAccountID, err) } - _, err = client.UpdateQueue(context.Background(), AccountIdentifier(testAccountID), UpdateQueueParams{}) + _, err = client.UpdateQueue(context.Background(), AccountIdentifier(testAccountID), UpdateQueueParams{Name: testQueueName}) if assert.Error(t, err) { assert.Equal(t, ErrMissingQueueName, err) } - results, err := client.UpdateQueue(context.Background(), AccountIdentifier(testAccountID), UpdateQueueParams{Name: "example-queue"}) + results, err := client.UpdateQueue(context.Background(), AccountIdentifier(testAccountID), UpdateQueueParams{Name: testQueueName, UpdatedName: "renamed-example-queue"}) if assert.NoError(t, err) { CreatedOn, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z") ModifiedOn, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z") diff --git a/r2_bucket.go b/r2_bucket.go index 08de630e1c..046438f094 100644 --- a/r2_bucket.go +++ b/r2_bucket.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "time" ) var ( @@ -14,8 +15,9 @@ var ( // R2Bucket defines a container for objects stored in R2 Storage. type R2Bucket struct { - Name string `json:"name"` - CreationDate string `json:"creation_date,omitempty"` + Name string `json:"name"` + CreationDate *time.Time `json:"creation_date,omitempty"` + Location string `json:"location,omitempty"` } // R2Buckets represents the map of buckets response from @@ -40,6 +42,16 @@ type ListR2BucketsParams struct { Cursor string `url:"cursor,omitempty"` } +type CreateR2BucketParameters struct { + Name string `json:"name,omitempty"` + LocationHint string `json:"locationHint,omitempty"` +} + +type R2BucketResponse struct { + Result R2Bucket `json:"result"` + Response +} + // ListR2Buckets Lists R2 buckets. func (api *API) ListR2Buckets(ctx context.Context, rc *ResourceContainer, params ListR2BucketsParams) ([]R2Bucket, error) { if rc.Identifier == "" { @@ -61,26 +73,58 @@ func (api *API) ListR2Buckets(ctx context.Context, rc *ResourceContainer, params return r2BucketListResponse.Result.Buckets, nil } -type CreateR2BucketParameters struct { - Name string `json:"name,omitempty"` -} - // CreateR2Bucket Creates a new R2 bucket. // // API reference: https://api.cloudflare.com/#r2-bucket-create-bucket -func (api *API) CreateR2Bucket(ctx context.Context, rc *ResourceContainer, params CreateR2BucketParameters) error { +func (api *API) CreateR2Bucket(ctx context.Context, rc *ResourceContainer, params CreateR2BucketParameters) (R2Bucket, error) { if rc.Identifier == "" { - return ErrMissingAccountID + return R2Bucket{}, ErrMissingAccountID } if params.Name == "" { - return ErrMissingBucketName + return R2Bucket{}, ErrMissingBucketName } uri := fmt.Sprintf("/accounts/%s/r2/buckets", rc.Identifier) - _, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return R2Bucket{}, err + } - return err + var r2BucketResponse R2BucketResponse + err = json.Unmarshal(res, &r2BucketResponse) + if err != nil { + return R2Bucket{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r2BucketResponse.Result, nil +} + +// GetR2Bucket Gets an existing R2 bucket. +// +// API reference: https://api.cloudflare.com/#r2-bucket-get-bucket +func (api *API) GetR2Bucket(ctx context.Context, rc *ResourceContainer, bucketName string) (R2Bucket, error) { + if rc.Identifier == "" { + return R2Bucket{}, ErrMissingAccountID + } + + if bucketName == "" { + return R2Bucket{}, ErrMissingBucketName + } + + uri := fmt.Sprintf("/accounts/%s/r2/buckets/%s", rc.Identifier, bucketName) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return R2Bucket{}, err + } + + var r2BucketResponse R2BucketResponse + err = json.Unmarshal(res, &r2BucketResponse) + if err != nil { + return R2Bucket{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r2BucketResponse.Result, nil } // DeleteR2Bucket Deletes an existing R2 bucket. diff --git a/r2_bucket_test.go b/r2_bucket_test.go index 42168838c8..ac9b2f9607 100644 --- a/r2_bucket_test.go +++ b/r2_bucket_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -32,11 +33,11 @@ func TestR2_ListBuckets(t *testing.T) { } }`) }) - + createDate, _ := time.Parse(time.RFC3339, "2022-06-24T19:58:49.477Z") want := []R2Bucket{ { Name: "example-bucket", - CreationDate: "2022-06-24T19:58:49.477Z", + CreationDate: &createDate, }, } actual, err := client.ListR2Buckets(context.Background(), AccountIdentifier(testAccountID), ListR2BucketsParams{}) @@ -45,6 +46,49 @@ func TestR2_ListBuckets(t *testing.T) { } } +func TestR2_GetBucket(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/accounts/%s/r2/buckets/%s", testAccountID, testBucketName), 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "name": "example-bucket", + "creation_date": "2022-06-24T19:58:49.477Z", + "location": "ENAM" + } +}`) + }) + + _, err := client.GetR2Bucket(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.GetR2Bucket(context.Background(), AccountIdentifier(testAccountID), "") + + if assert.Error(t, err) { + assert.Equal(t, ErrMissingBucketName, err) + } + + createDate, _ := time.Parse(time.RFC3339, "2022-06-24T19:58:49.477Z") + want := R2Bucket{ + Name: testBucketName, + CreationDate: &createDate, + Location: "ENAM", + } + + actual, err := client.GetR2Bucket(context.Background(), AccountIdentifier(testAccountID), testBucketName) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestR2_CreateBucket(t *testing.T) { setup() defer teardown() @@ -56,22 +100,34 @@ func TestR2_CreateBucket(t *testing.T) { "success": true, "errors": [], "messages": [], - "result": {} + "result": { + "name": "example-bucket", + "creation_date": "2022-06-24T19:58:49.477Z", + "location": "ENAM" + } }`) }) - err := client.CreateR2Bucket(context.Background(), AccountIdentifier(""), CreateR2BucketParameters{}) + _, err := client.CreateR2Bucket(context.Background(), AccountIdentifier(""), CreateR2BucketParameters{}) if assert.Error(t, err) { assert.Equal(t, ErrMissingAccountID, err) } - err = client.CreateR2Bucket(context.Background(), AccountIdentifier(testAccountID), CreateR2BucketParameters{Name: ""}) + _, err = client.CreateR2Bucket(context.Background(), AccountIdentifier(testAccountID), CreateR2BucketParameters{Name: ""}) if assert.Error(t, err) { assert.Equal(t, ErrMissingBucketName, err) } + createDate, _ := time.Parse(time.RFC3339, "2022-06-24T19:58:49.477Z") + want := R2Bucket{ + Name: testBucketName, + CreationDate: &createDate, + Location: "ENAM", + } - err = client.CreateR2Bucket(context.Background(), AccountIdentifier(testAccountID), CreateR2BucketParameters{Name: testBucketName}) - assert.NoError(t, err) + actual, err := client.CreateR2Bucket(context.Background(), AccountIdentifier(testAccountID), CreateR2BucketParameters{Name: testBucketName, LocationHint: "ENAM"}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } } func TestR2_DeleteBucket(t *testing.T) { diff --git a/regional_hostnames.go b/regional_hostnames.go new file mode 100644 index 0000000000..72d6dcc119 --- /dev/null +++ b/regional_hostnames.go @@ -0,0 +1,190 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type Region struct { + Key string `json:"key"` + Label string `json:"label"` +} + +type RegionalHostname struct { + Hostname string `json:"hostname"` + RegionKey string `json:"region_key"` + CreatedOn *time.Time `json:"created_on,omitempty"` +} + +// regionalHostnameResponse contains an API Response from a Create, Get, Update, or Delete call. +type regionalHostnameResponse struct { + Response + Result RegionalHostname `json:"result"` +} + +type ListDataLocalizationRegionsParams struct{} +type ListDataLocalizationRegionalHostnamesParams struct{} + +type CreateDataLocalizationRegionalHostnameParams struct { + Hostname string `json:"hostname"` + RegionKey string `json:"region_key"` +} + +type UpdateDataLocalizationRegionalHostnameParams struct { + Hostname string `json:"-"` + RegionKey string `json:"region_key"` +} + +// ListDataLocalizationRegions lists all available regions. +// +// API reference: https://developers.cloudflare.com/data-localization/regional-services/get-started/#configure-regional-services-via-api +func (api *API) ListDataLocalizationRegions(ctx context.Context, rc *ResourceContainer, params ListDataLocalizationRegionsParams) ([]Region, error) { + if rc.Level != AccountRouteLevel { + return []Region{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + if rc.Identifier == "" { + return []Region{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/addressing/regional_hostnames/regions", rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []Region{}, err + } + result := struct { + Result []Region `json:"result"` + }{} + if err := json.Unmarshal(res, &result); err != nil { + return []Region{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// ListDataLocalizationRegionalHostnames lists all regional hostnames for a zone. +// +// API reference: https://developers.cloudflare.com/data-localization/regional-services/get-started/#configure-regional-services-via-api +func (api *API) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *ResourceContainer, params ListDataLocalizationRegionalHostnamesParams) ([]RegionalHostname, error) { + if rc.Level != ZoneRouteLevel { + return []RegionalHostname{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + if rc.Identifier == "" { + return []RegionalHostname{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/addressing/regional_hostnames", rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []RegionalHostname{}, err + } + result := struct { + Result []RegionalHostname `json:"result"` + }{} + if err := json.Unmarshal(res, &result); err != nil { + return []RegionalHostname{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// CreateDataLocalizationRegionalHostname lists all regional hostnames for a zone. +// +// API reference: https://developers.cloudflare.com/data-localization/regional-services/get-started/#configure-regional-services-via-api +func (api *API) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *ResourceContainer, params CreateDataLocalizationRegionalHostnameParams) (RegionalHostname, error) { + if rc.Level != ZoneRouteLevel { + return RegionalHostname{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + if rc.Identifier == "" { + return RegionalHostname{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/addressing/regional_hostnames", rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return RegionalHostname{}, err + } + result := regionalHostnameResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return RegionalHostname{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// GetDataLocalizationRegionalHostname returns the details of a specific regional hostname. +// +// API reference: https://developers.cloudflare.com/data-localization/regional-services/get-started/#configure-regional-services-via-api +func (api *API) GetDataLocalizationRegionalHostname(ctx context.Context, rc *ResourceContainer, hostname string) (RegionalHostname, error) { + if rc.Level != ZoneRouteLevel { + return RegionalHostname{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + if rc.Identifier == "" { + return RegionalHostname{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/addressing/regional_hostnames/%s", rc.Identifier, hostname) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return RegionalHostname{}, err + } + + result := regionalHostnameResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return RegionalHostname{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// UpdateDataLocalizationRegionalHostname returns the details of a specific regional hostname. +// +// API reference: https://developers.cloudflare.com/data-localization/regional-services/get-started/#configure-regional-services-via-api +func (api *API) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *ResourceContainer, params UpdateDataLocalizationRegionalHostnameParams) (RegionalHostname, error) { + if rc.Level != ZoneRouteLevel { + return RegionalHostname{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + if rc.Identifier == "" { + return RegionalHostname{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/addressing/regional_hostnames/%s", rc.Identifier, params.Hostname) + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params) + if err != nil { + return RegionalHostname{}, err + } + result := regionalHostnameResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return RegionalHostname{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// DeleteDataLocalizationRegionalHostname deletes a regional hostname. +// +// API reference: https://developers.cloudflare.com/data-localization/regional-services/get-started/#configure-regional-services-via-api +func (api *API) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *ResourceContainer, hostname string) error { + if rc.Level != ZoneRouteLevel { + return fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + if rc.Identifier == "" { + return ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/addressing/regional_hostnames/%s", rc.Identifier, hostname) + + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return err + } + return nil +} diff --git a/regional_hostnames_test.go b/regional_hostnames_test.go new file mode 100644 index 0000000000..0391d3ccbc --- /dev/null +++ b/regional_hostnames_test.go @@ -0,0 +1,219 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const regionalHostname = "eu.example.com" + +func TestListRegions(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "result": [ + { + "key": "ca", + "label": "Canada" + }, + { + "key": "eu", + "label": "Europe" + } + ], + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/addressing/regional_hostnames/regions", handler) + + want := []Region{ + { + Key: "ca", + Label: "Canada", + }, + { + Key: "eu", + Label: "Europe", + }, + } + + actual, err := client.ListDataLocalizationRegions(context.Background(), AccountIdentifier(testAccountID), ListDataLocalizationRegionsParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListRegionalHostnames(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "result": [ + { + "hostname": "%s", + "region_key": "ca", + "created_on": "2023-01-14T00:47:57.060267Z" + } + ], + "success": true, + "errors": [], + "messages": [] + }`, regionalHostname) + } + + mux.HandleFunc("/zones/"+testZoneID+"/addressing/regional_hostnames", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-14T00:47:57.060267Z") + want := []RegionalHostname{ + { + Hostname: regionalHostname, + RegionKey: "ca", + CreatedOn: &createdOn, + }, + } + + actual, err := client.ListDataLocalizationRegionalHostnames(context.Background(), ZoneIdentifier(testZoneID), ListDataLocalizationRegionalHostnamesParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateRegionalHostname(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "result": { + "hostname": "%s", + "region_key": "ca", + "created_on": "2023-01-14T00:47:57.060267Z" + }, + "success": true, + "errors": [], + "messages": [] + }`, regionalHostname) + } + + mux.HandleFunc("/zones/"+testZoneID+"/addressing/regional_hostnames", handler) + + params := CreateDataLocalizationRegionalHostnameParams{ + RegionKey: "ca", + Hostname: regionalHostname, + } + + want := RegionalHostname{ + RegionKey: "ca", + Hostname: regionalHostname, + } + + actual, err := client.CreateDataLocalizationRegionalHostname(context.Background(), ZoneIdentifier(testZoneID), params) + createdOn, _ := time.Parse(time.RFC3339, "2023-01-14T00:47:57.060267Z") + want.CreatedOn = &createdOn + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetRegionalHostname(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "result": { + "hostname": "%s", + "region_key": "ca", + "created_on": "2023-01-14T00:47:57.060267Z" + }, + "success": true, + "errors": [], + "messages": [] + }`, regionalHostname) + } + + mux.HandleFunc("/zones/"+testZoneID+"/addressing/regional_hostnames/"+regionalHostname, handler) + + actual, err := client.GetDataLocalizationRegionalHostname(context.Background(), ZoneIdentifier(testZoneID), regionalHostname) + createdOn, _ := time.Parse(time.RFC3339, "2023-01-14T00:47:57.060267Z") + want := RegionalHostname{ + RegionKey: "ca", + Hostname: regionalHostname, + CreatedOn: &createdOn, + } + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateRegionalHostname(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "result": { + "hostname": "%s", + "region_key": "eu", + "created_on": "2023-01-14T00:47:57.060267Z" + }, + "success": true, + "errors": [], + "messages": [] + }`, regionalHostname) + } + + params := UpdateDataLocalizationRegionalHostnameParams{ + RegionKey: "eu", + Hostname: regionalHostname, + } + + want := RegionalHostname{ + RegionKey: "eu", + Hostname: regionalHostname, + } + + mux.HandleFunc("/zones/"+testZoneID+"/addressing/regional_hostnames/"+regionalHostname, handler) + + actual, err := client.UpdateDataLocalizationRegionalHostname(context.Background(), ZoneIdentifier(testZoneID), params) + createdOn, _ := time.Parse(time.RFC3339, "2023-01-14T00:47:57.060267Z") + want.CreatedOn = &createdOn + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteRegionalHostname(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method) + } + + mux.HandleFunc("/zones/"+testZoneID+"/addressing/regional_hostnames/"+regionalHostname, handler) + + err := client.DeleteDataLocalizationRegionalHostname(context.Background(), ZoneIdentifier(testZoneID), regionalHostname) + assert.NoError(t, err) +} diff --git a/resource.go b/resource.go index cf0a9cfb6b..2113e5eba7 100644 --- a/resource.go +++ b/resource.go @@ -1,5 +1,7 @@ package cloudflare +import "fmt" + // RouteLevel holds the "level" where the resource resides. type RouteLevel string @@ -17,6 +19,17 @@ type ResourceContainer struct { Identifier string } +// Returns a URL fragment of the endpoint scoped by the container. +// +// For example, a zone identifier would have a fragment like "zones/foobar" while +// an account identifier would generate "accounts/foobar". +func (rc *ResourceContainer) URLFragment() string { + if rc.Level == "" { + return rc.Identifier + } + return fmt.Sprintf("%s/%s", rc.Level, rc.Identifier) +} + // ResourceIdentifier returns a generic *ResourceContainer. func ResourceIdentifier(id string) *ResourceContainer { return &ResourceContainer{ diff --git a/resource_test.go b/resource_test.go new file mode 100644 index 0000000000..ef0c96d312 --- /dev/null +++ b/resource_test.go @@ -0,0 +1,25 @@ +package cloudflare + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourcURLFragment(t *testing.T) { + tests := map[string]struct { + container *ResourceContainer + want string + }{ + "account resource": {container: AccountIdentifier("foo"), want: "accounts/foo"}, + "zone resource": {container: ZoneIdentifier("foo"), want: "zones/foo"}, + "missing level resource": {container: &ResourceContainer{Level: "", Identifier: "foo"}, want: "foo"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.container.URLFragment() + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/rulesets.go b/rulesets.go index 4a15f579fb..72a7a4f3bf 100644 --- a/rulesets.go +++ b/rulesets.go @@ -35,11 +35,13 @@ const ( RulesetPhaseHTTPResponseFirewallManaged RulesetPhase = "http_response_firewall_managed" RulesetPhaseHTTPResponseHeadersTransform RulesetPhase = "http_response_headers_transform" RulesetPhaseHTTPResponseHeadersTransformManaged RulesetPhase = "http_response_headers_transform_managed" + RulesetPhaseHTTPResponseCompression RulesetPhase = "http_response_compression" RulesetPhaseMagicTransit RulesetPhase = "magic_transit" RulesetPhaseRateLimit RulesetPhase = "http_ratelimit" RulesetPhaseSuperBotFightMode RulesetPhase = "http_request_sbfm" RulesetPhaseHTTPConfigSettings RulesetPhase = "http_config_settings" + RulesetRuleActionAllow RulesetRuleAction = "allow" RulesetRuleActionBlock RulesetRuleAction = "block" RulesetRuleActionChallenge RulesetRuleAction = "challenge" RulesetRuleActionDDoSDynamic RulesetRuleAction = "ddos_dynamic" @@ -57,6 +59,7 @@ const ( RulesetRuleActionSetConfig RulesetRuleAction = "set_config" RulesetRuleActionServeError RulesetRuleAction = "serve_error" RulesetRuleActionSkip RulesetRuleAction = "skip" + RulesetRuleActionCompressResponse RulesetRuleAction = "compress_response" RulesetActionParameterProductBIC RulesetActionParameterProduct = "bic" RulesetActionParameterProductHOT RulesetActionParameterProduct = "hot" @@ -68,6 +71,7 @@ const ( RulesetRuleActionParametersHTTPHeaderOperationRemove RulesetRuleActionParametersHTTPHeaderOperation = "remove" RulesetRuleActionParametersHTTPHeaderOperationSet RulesetRuleActionParametersHTTPHeaderOperation = "set" + RulesetRuleActionParametersHTTPHeaderOperationAdd RulesetRuleActionParametersHTTPHeaderOperation = "add" ) // RulesetKindValues exposes all the available `RulesetKind` values as a slice @@ -104,6 +108,7 @@ func RulesetPhaseValues() []string { string(RulesetPhaseHTTPResponseFirewallManaged), string(RulesetPhaseHTTPResponseHeadersTransform), string(RulesetPhaseHTTPResponseHeadersTransformManaged), + string(RulesetPhaseHTTPResponseCompression), string(RulesetPhaseMagicTransit), string(RulesetPhaseRateLimit), string(RulesetPhaseSuperBotFightMode), @@ -115,6 +120,7 @@ func RulesetPhaseValues() []string { // as a slice of strings. func RulesetRuleActionValues() []string { return []string{ + string(RulesetRuleActionAllow), string(RulesetRuleActionBlock), string(RulesetRuleActionChallenge), string(RulesetRuleActionDDoSDynamic), @@ -132,6 +138,7 @@ func RulesetRuleActionValues() []string { string(RulesetRuleActionSetConfig), string(RulesetRuleActionServeError), string(RulesetRuleActionSkip), + string(RulesetRuleActionCompressResponse), } } @@ -153,6 +160,7 @@ func RulesetRuleActionParametersHTTPHeaderOperationValues() []string { return []string{ string(RulesetRuleActionParametersHTTPHeaderOperationRemove), string(RulesetRuleActionParametersHTTPHeaderOperationSet), + string(RulesetRuleActionParametersHTTPHeaderOperationAdd), } } @@ -185,7 +193,7 @@ type Ruleset struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Kind string `json:"kind,omitempty"` - Version string `json:"version,omitempty"` + Version *string `json:"version,omitempty"` LastUpdated *time.Time `json:"last_updated,omitempty"` Phase string `json:"phase,omitempty"` Rules []RulesetRule `json:"rules"` @@ -201,53 +209,54 @@ type RulesetActionParametersLogCustomField struct { // RulesetRuleActionParameters specifies the action parameters for a Ruleset // rule. type RulesetRuleActionParameters struct { - ID string `json:"id,omitempty"` - Ruleset string `json:"ruleset,omitempty"` - Rulesets []string `json:"rulesets,omitempty"` - Rules map[string][]string `json:"rules,omitempty"` - Increment int `json:"increment,omitempty"` - URI *RulesetRuleActionParametersURI `json:"uri,omitempty"` - Headers map[string]RulesetRuleActionParametersHTTPHeader `json:"headers,omitempty"` - Products []string `json:"products,omitempty"` - Phases []string `json:"phases,omitempty"` - Overrides *RulesetRuleActionParametersOverrides `json:"overrides,omitempty"` - MatchedData *RulesetRuleActionParametersMatchedData `json:"matched_data,omitempty"` - Version string `json:"version,omitempty"` - Response *RulesetRuleActionParametersBlockResponse `json:"response,omitempty"` - HostHeader string `json:"host_header,omitempty"` - Origin *RulesetRuleActionParametersOrigin `json:"origin,omitempty"` - SNI *RulesetRuleActionParametersSni `json:"sni,omitempty"` - RequestFields []RulesetActionParametersLogCustomField `json:"request_fields,omitempty"` - ResponseFields []RulesetActionParametersLogCustomField `json:"response_fields,omitempty"` - CookieFields []RulesetActionParametersLogCustomField `json:"cookie_fields,omitempty"` - Cache *bool `json:"cache,omitempty"` - EdgeTTL *RulesetRuleActionParametersEdgeTTL `json:"edge_ttl,omitempty"` - BrowserTTL *RulesetRuleActionParametersBrowserTTL `json:"browser_ttl,omitempty"` - ServeStale *RulesetRuleActionParametersServeStale `json:"serve_stale,omitempty"` - Content string `json:"content,omitempty"` - ContentType string `json:"content_type,omitempty"` - StatusCode uint16 `json:"status_code,omitempty"` - RespectStrongETags *bool `json:"respect_strong_etags,omitempty"` - CacheKey *RulesetRuleActionParametersCacheKey `json:"cache_key,omitempty"` - OriginErrorPagePassthru *bool `json:"origin_error_page_passthru,omitempty"` - FromList *RulesetRuleActionParametersFromList `json:"from_list,omitempty"` - FromValue *RulesetRuleActionParametersFromValue `json:"from_value,omitempty"` - AutomaticHTTPSRewrites *bool `json:"automatic_https_rewrites,omitempty"` - AutoMinify *RulesetRuleActionParametersAutoMinify `json:"autominify,omitempty"` - BrowserIntegrityCheck *bool `json:"bic,omitempty"` - DisableApps *bool `json:"disable_apps,omitempty"` - DisableZaraz *bool `json:"disable_zaraz,omitempty"` - DisableRailgun *bool `json:"disable_railgun,omitempty"` - EmailObfuscation *bool `json:"email_obfuscation,omitempty"` - Mirage *bool `json:"mirage,omitempty"` - OpportunisticEncryption *bool `json:"opportunistic_encryption,omitempty"` - Polish *Polish `json:"polish,omitempty"` - RocketLoader *bool `json:"rocket_loader,omitempty"` - SecurityLevel *SecurityLevel `json:"security_level,omitempty"` - ServerSideExcludes *bool `json:"server_side_excludes,omitempty"` - SSL *SSL `json:"ssl,omitempty"` - SXG *bool `json:"sxg,omitempty"` - HotLinkProtection *bool `json:"hotlink_protection,omitempty"` + ID string `json:"id,omitempty"` + Ruleset string `json:"ruleset,omitempty"` + Rulesets []string `json:"rulesets,omitempty"` + Rules map[string][]string `json:"rules,omitempty"` + Increment int `json:"increment,omitempty"` + URI *RulesetRuleActionParametersURI `json:"uri,omitempty"` + Headers map[string]RulesetRuleActionParametersHTTPHeader `json:"headers,omitempty"` + Products []string `json:"products,omitempty"` + Phases []string `json:"phases,omitempty"` + Overrides *RulesetRuleActionParametersOverrides `json:"overrides,omitempty"` + MatchedData *RulesetRuleActionParametersMatchedData `json:"matched_data,omitempty"` + Version *string `json:"version,omitempty"` + Response *RulesetRuleActionParametersBlockResponse `json:"response,omitempty"` + HostHeader string `json:"host_header,omitempty"` + Origin *RulesetRuleActionParametersOrigin `json:"origin,omitempty"` + SNI *RulesetRuleActionParametersSni `json:"sni,omitempty"` + RequestFields []RulesetActionParametersLogCustomField `json:"request_fields,omitempty"` + ResponseFields []RulesetActionParametersLogCustomField `json:"response_fields,omitempty"` + CookieFields []RulesetActionParametersLogCustomField `json:"cookie_fields,omitempty"` + Cache *bool `json:"cache,omitempty"` + EdgeTTL *RulesetRuleActionParametersEdgeTTL `json:"edge_ttl,omitempty"` + BrowserTTL *RulesetRuleActionParametersBrowserTTL `json:"browser_ttl,omitempty"` + ServeStale *RulesetRuleActionParametersServeStale `json:"serve_stale,omitempty"` + Content string `json:"content,omitempty"` + ContentType string `json:"content_type,omitempty"` + StatusCode uint16 `json:"status_code,omitempty"` + RespectStrongETags *bool `json:"respect_strong_etags,omitempty"` + CacheKey *RulesetRuleActionParametersCacheKey `json:"cache_key,omitempty"` + OriginErrorPagePassthru *bool `json:"origin_error_page_passthru,omitempty"` + FromList *RulesetRuleActionParametersFromList `json:"from_list,omitempty"` + FromValue *RulesetRuleActionParametersFromValue `json:"from_value,omitempty"` + AutomaticHTTPSRewrites *bool `json:"automatic_https_rewrites,omitempty"` + AutoMinify *RulesetRuleActionParametersAutoMinify `json:"autominify,omitempty"` + BrowserIntegrityCheck *bool `json:"bic,omitempty"` + DisableApps *bool `json:"disable_apps,omitempty"` + DisableZaraz *bool `json:"disable_zaraz,omitempty"` + DisableRailgun *bool `json:"disable_railgun,omitempty"` + EmailObfuscation *bool `json:"email_obfuscation,omitempty"` + Mirage *bool `json:"mirage,omitempty"` + OpportunisticEncryption *bool `json:"opportunistic_encryption,omitempty"` + Polish *Polish `json:"polish,omitempty"` + RocketLoader *bool `json:"rocket_loader,omitempty"` + SecurityLevel *SecurityLevel `json:"security_level,omitempty"` + ServerSideExcludes *bool `json:"server_side_excludes,omitempty"` + SSL *SSL `json:"ssl,omitempty"` + SXG *bool `json:"sxg,omitempty"` + HotLinkProtection *bool `json:"hotlink_protection,omitempty"` + Algorithms []RulesetRuleActionParametersCompressionAlgorithm `json:"algorithms,omitempty"` } // RulesetRuleActionParametersFromList holds the FromList struct for @@ -266,7 +275,7 @@ type RulesetRuleActionParametersAutoMinify struct { type RulesetRuleActionParametersFromValue struct { StatusCode uint16 `json:"status_code,omitempty"` TargetURL RulesetRuleActionParametersTargetURL `json:"target_url"` - PreserveQueryString bool `json:"preserve_query_string,omitempty"` + PreserveQueryString *bool `json:"preserve_query_string,omitempty"` } type RulesetRuleActionParametersTargetURL struct { @@ -330,6 +339,7 @@ type RulesetRuleActionParametersCustomKeyFields struct { type RulesetRuleActionParametersCustomKeyQuery struct { Include *RulesetRuleActionParametersCustomKeyList `json:"include,omitempty"` Exclude *RulesetRuleActionParametersCustomKeyList `json:"exclude,omitempty"` + Ignore *bool `json:"ignore,omitempty"` } type RulesetRuleActionParametersCustomKeyList struct { @@ -380,7 +390,7 @@ type RulesetRuleActionParametersBlockResponse struct { type RulesetRuleActionParametersURI struct { Path *RulesetRuleActionParametersURIPath `json:"path,omitempty"` Query *RulesetRuleActionParametersURIQuery `json:"query,omitempty"` - Origin bool `json:"origin,omitempty"` + Origin *bool `json:"origin,omitempty"` } // RulesetRuleActionParametersURIPath holds the path specific portion of a URI @@ -393,8 +403,8 @@ type RulesetRuleActionParametersURIPath struct { // RulesetRuleActionParametersURIQuery holds the query specific portion of a URI // action parameter. type RulesetRuleActionParametersURIQuery struct { - Value string `json:"value,omitempty"` - Expression string `json:"expression,omitempty"` + Value *string `json:"value,omitempty"` + Expression string `json:"expression,omitempty"` } // RulesetRuleActionParametersHTTPHeader is the definition for define action @@ -446,6 +456,12 @@ type RulesetRuleActionParametersSni struct { Value string `json:"value"` } +// RulesetRuleActionParametersCompressionAlgorithm defines a compression +// algorithm for the compress_response action. +type RulesetRuleActionParametersCompressionAlgorithm struct { + Name string `json:"name"` +} + type Polish int const ( @@ -626,14 +642,14 @@ func (p SSL) IntoRef() *SSL { // RulesetRule contains information about a single Ruleset Rule. type RulesetRule struct { ID string `json:"id,omitempty"` - Version string `json:"version,omitempty"` + Version *string `json:"version,omitempty"` Action string `json:"action"` ActionParameters *RulesetRuleActionParameters `json:"action_parameters,omitempty"` Expression string `json:"expression"` - Description string `json:"description"` + Description string `json:"description,omitempty"` LastUpdated *time.Time `json:"last_updated,omitempty"` Ref string `json:"ref,omitempty"` - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled,omitempty"` ScoreThreshold int `json:"score_threshold,omitempty"` RateLimit *RulesetRuleRateLimit `json:"ratelimit,omitempty"` ExposedCredentialCheck *RulesetRuleExposedCredentialCheck `json:"exposed_credential_check,omitempty"` @@ -642,12 +658,14 @@ type RulesetRule struct { // RulesetRuleRateLimit contains the structure of a HTTP rate limit Ruleset Rule. type RulesetRuleRateLimit struct { - Characteristics []string `json:"characteristics,omitempty"` - RequestsPerPeriod int `json:"requests_per_period,omitempty"` - Period int `json:"period,omitempty"` - MitigationTimeout int `json:"mitigation_timeout,omitempty"` - CountingExpression string `json:"counting_expression,omitempty"` - RequestsToOrigin bool `json:"requests_to_origin,omitempty"` + Characteristics []string `json:"characteristics,omitempty"` + RequestsPerPeriod int `json:"requests_per_period,omitempty"` + ScorePerPeriod int `json:"score_per_period,omitempty"` + ScoreResponseHeaderName string `json:"score_response_header_name,omitempty"` + Period int `json:"period,omitempty"` + MitigationTimeout int `json:"mitigation_timeout,omitempty"` + CountingExpression string `json:"counting_expression,omitempty"` + RequestsToOrigin bool `json:"requests_to_origin,omitempty"` } // RulesetRuleExposedCredentialCheck contains the structure of an exposed diff --git a/rulesets_test.go b/rulesets_test.go index a2b33ad079..52e9ed88ce 100644 --- a/rulesets_test.go +++ b/rulesets_test.go @@ -46,7 +46,7 @@ func TestListRulesets(t *testing.T) { Name: "my example ruleset", Description: "Test magic transit ruleset", Kind: "root", - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseMagicTransit), }, @@ -96,7 +96,7 @@ func TestGetRuleset_MagicTransit(t *testing.T) { Name: "my example ruleset", Description: "Test magic transit ruleset", Kind: "root", - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseMagicTransit), } @@ -161,20 +161,20 @@ func TestGetRuleset_WAF(t *testing.T) { rules := []RulesetRule{{ ID: "78723a9e0c7c4c6dbec5684cb766231d", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionRewrite), ActionParameters: &RulesetRuleActionParameters{ URI: &RulesetRuleActionParametersURI{ Path: &RulesetRuleActionParametersURIPath{ Expression: "normalize_url_path(raw.http.request.uri.path)", }, - Origin: false, + Origin: BoolPtr(false), }, }, Description: "Normalization on the URL path, without propagating it to the origin", LastUpdated: &lastUpdated, Ref: "272936dc447b41fe976255ff6b768ec0", - Enabled: true, + Enabled: BoolPtr(true), }} want := Ruleset{ @@ -182,7 +182,7 @@ func TestGetRuleset_WAF(t *testing.T) { Name: "Cloudflare Normalization Ruleset", Description: "Created by the Cloudflare security team, this ruleset provides normalization on the URL path", Kind: string(RulesetKindManaged), - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseHTTPRequestSanitize), Rules: rules, @@ -265,7 +265,7 @@ func TestGetRuleset_SetCacheSettings(t *testing.T) { rules := []RulesetRule{{ ID: "78723a9e0c7c4c6dbec5684cb766231d", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionSetCacheSettings), ActionParameters: &RulesetRuleActionParameters{ Cache: BoolPtr(true), @@ -329,7 +329,7 @@ func TestGetRuleset_SetCacheSettings(t *testing.T) { Description: "Set all available cache settings in one rule", LastUpdated: &lastUpdated, Ref: "272936dc447b41fe976255ff6b768ec0", - Enabled: true, + Enabled: BoolPtr(true), }} want := Ruleset{ @@ -337,7 +337,7 @@ func TestGetRuleset_SetCacheSettings(t *testing.T) { Name: "Cloudflare Cache Rules Ruleset", Description: "This ruleset provides cache settings modifications", Kind: string(RulesetKindZone), - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseHTTPRequestCacheSettings), Rules: rules, @@ -413,7 +413,7 @@ func TestGetRuleset_SetConfig(t *testing.T) { rules := []RulesetRule{{ ID: "78723a9e0c7c4c6dbec5684cb766231d", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionSetConfig), ActionParameters: &RulesetRuleActionParameters{ AutomaticHTTPSRewrites: BoolPtr(true), @@ -440,7 +440,7 @@ func TestGetRuleset_SetConfig(t *testing.T) { Description: "Set all available config rules in one rule", LastUpdated: &lastUpdated, Ref: "272936dc447b41fe976255ff6b768ec0", - Enabled: true, + Enabled: BoolPtr(true), }} want := Ruleset{ @@ -448,7 +448,7 @@ func TestGetRuleset_SetConfig(t *testing.T) { Name: "Cloudflare Config Rules Ruleset", Description: "This ruleset provides config rules modifications", Kind: string(RulesetKindZone), - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseHTTPConfigSettings), Rules: rules, @@ -515,7 +515,7 @@ func TestGetRuleset_RedirectFromValue(t *testing.T) { rules := []RulesetRule{{ ID: "78723a9e0c7c4c6dbec5684cb766231d", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionRedirect), ActionParameters: &RulesetRuleActionParameters{ FromValue: &RulesetRuleActionParametersFromValue{ @@ -523,13 +523,13 @@ func TestGetRuleset_RedirectFromValue(t *testing.T) { TargetURL: RulesetRuleActionParametersTargetURL{ Value: "some_host.com", }, - PreserveQueryString: true, + PreserveQueryString: BoolPtr(true), }, }, Description: "Set dynamic redirect from value", LastUpdated: &lastUpdated, Ref: "272936dc447b41fe976255ff6b768ec0", - Enabled: true, + Enabled: BoolPtr(true), }} want := Ruleset{ @@ -537,7 +537,7 @@ func TestGetRuleset_RedirectFromValue(t *testing.T) { Name: "Cloudflare Redirect Rules Ruleset", Description: "This ruleset provides redirect from value", Kind: string(RulesetKindZone), - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseHTTPRequestDynamicRedirect), Rules: rules, @@ -554,6 +554,86 @@ func TestGetRuleset_RedirectFromValue(t *testing.T) { } } +func TestGetRuleset_CompressResponse(t *testing.T) { + setup() + defer teardown() + + handler := 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, `{ + "result": { + "id": "70339d97bdb34195bbf054b1ebe81f76", + "name": "Cloudflare compress response ruleset", + "description": "This ruleset provides response compression rules", + "kind": "zone", + "version": "1", + "rules": [ + { + "id": "78723a9e0c7c4c6dbec5684cb766231d", + "version": "1", + "action": "compress_response", + "action_parameters": { + "algorithms": [ { "name": "brotli" }, { "name": "default" } ] + }, + "description": "Compress response rule", + "last_updated": "2020-12-18T09:28:09.655749Z", + "ref": "272936dc447b41fe976255ff6b768ec0", + "enabled": true + } + ], + "last_updated": "2020-12-18T09:28:09.655749Z", + "phase": "http_response_compression" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rulesets/b232b534beea4e00a21dcbb7a8a545e9", handler) + mux.HandleFunc("/zones/"+testZoneID+"/rulesets/b232b534beea4e00a21dcbb7a8a545e9", handler) + + lastUpdated, _ := time.Parse(time.RFC3339, "2020-12-18T09:28:09.655749Z") + + rules := []RulesetRule{{ + ID: "78723a9e0c7c4c6dbec5684cb766231d", + Version: StringPtr("1"), + Action: string(RulesetRuleActionCompressResponse), + ActionParameters: &RulesetRuleActionParameters{ + Algorithms: []RulesetRuleActionParametersCompressionAlgorithm{ + {Name: "brotli"}, + {Name: "default"}, + }, + }, + Description: "Compress response rule", + LastUpdated: &lastUpdated, + Ref: "272936dc447b41fe976255ff6b768ec0", + Enabled: BoolPtr(true), + }} + + want := Ruleset{ + ID: "70339d97bdb34195bbf054b1ebe81f76", + Name: "Cloudflare compress response ruleset", + Description: "This ruleset provides response compression rules", + Kind: string(RulesetKindZone), + Version: StringPtr("1"), + LastUpdated: &lastUpdated, + Phase: string(RulesetPhaseHTTPResponseCompression), + Rules: rules, + } + + zoneActual, err := client.GetZoneRuleset(context.Background(), testZoneID, "b232b534beea4e00a21dcbb7a8a545e9") + if assert.NoError(t, err) { + assert.Equal(t, want, zoneActual) + } + + accountActual, err := client.GetAccountRuleset(context.Background(), testAccountID, "b232b534beea4e00a21dcbb7a8a545e9") + if assert.NoError(t, err) { + assert.Equal(t, want, accountActual) + } +} + func TestCreateRuleset(t *testing.T) { setup() defer teardown() @@ -599,7 +679,7 @@ func TestCreateRuleset(t *testing.T) { rules := []RulesetRule{{ ID: "62449e2e0de149619edb35e59c10d801", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionSkip), ActionParameters: &RulesetRuleActionParameters{ Ruleset: "current", @@ -608,7 +688,7 @@ func TestCreateRuleset(t *testing.T) { Description: "Allow TCP Ephemeral Ports", LastUpdated: &lastUpdated, Ref: "72449e2e0de149619edb35e59c10d801", - Enabled: true, + Enabled: BoolPtr(true), }} newRuleset := Ruleset{ @@ -624,7 +704,7 @@ func TestCreateRuleset(t *testing.T) { Name: "my example ruleset", Description: "Test magic transit ruleset", Kind: "root", - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseMagicTransit), Rules: rules, @@ -719,7 +799,7 @@ func TestUpdateRuleset(t *testing.T) { rules := []RulesetRule{{ ID: "62449e2e0de149619edb35e59c10d801", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionSkip), ActionParameters: &RulesetRuleActionParameters{ Ruleset: "current", @@ -728,10 +808,10 @@ func TestUpdateRuleset(t *testing.T) { Description: "Allow TCP Ephemeral Ports", LastUpdated: &lastUpdated, Ref: "72449e2e0de149619edb35e59c10d801", - Enabled: true, + Enabled: BoolPtr(true), }, { ID: "62449e2e0de149619edb35e59c10d802", - Version: "1", + Version: StringPtr("1"), Action: string(RulesetRuleActionSkip), ActionParameters: &RulesetRuleActionParameters{ Ruleset: "current", @@ -740,7 +820,7 @@ func TestUpdateRuleset(t *testing.T) { Description: "Allow UDP Ephemeral Ports", LastUpdated: &lastUpdated, Ref: "72449e2e0de149619edb35e59c10d801", - Enabled: true, + Enabled: BoolPtr(true), }} want := Ruleset{ @@ -748,7 +828,7 @@ func TestUpdateRuleset(t *testing.T) { Name: "ruleset1", Description: "Test Firewall Ruleset Update", Kind: "root", - Version: "1", + Version: StringPtr("1"), LastUpdated: &lastUpdated, Phase: string(RulesetPhaseMagicTransit), Rules: rules, diff --git a/ssl.go b/ssl.go index 125ab02ac8..178e98c6ca 100644 --- a/ssl.go +++ b/ssl.go @@ -10,19 +10,19 @@ import ( // ZoneCustomSSL represents custom SSL certificate metadata. type ZoneCustomSSL struct { - ID string `json:"id"` - Hosts []string `json:"hosts"` - Issuer string `json:"issuer"` - Signature string `json:"signature"` - Status string `json:"status"` - BundleMethod string `json:"bundle_method"` - GeoRestrictions ZoneCustomSSLGeoRestrictions `json:"geo_restrictions"` - ZoneID string `json:"zone_id"` - UploadedOn time.Time `json:"uploaded_on"` - ModifiedOn time.Time `json:"modified_on"` - ExpiresOn time.Time `json:"expires_on"` - Priority int `json:"priority"` - KeylessServer KeylessSSL `json:"keyless_server"` + ID string `json:"id"` + Hosts []string `json:"hosts"` + Issuer string `json:"issuer"` + Signature string `json:"signature"` + Status string `json:"status"` + BundleMethod string `json:"bundle_method"` + GeoRestrictions *ZoneCustomSSLGeoRestrictions `json:"geo_restrictions,omitempty"` + ZoneID string `json:"zone_id"` + UploadedOn time.Time `json:"uploaded_on"` + ModifiedOn time.Time `json:"modified_on"` + ExpiresOn time.Time `json:"expires_on"` + Priority int `json:"priority"` + KeylessServer KeylessSSL `json:"keyless_server"` } // ZoneCustomSSLGeoRestrictions represents the parameter to create or update diff --git a/ssl_test.go b/ssl_test.go index 78dd51022f..ea43e63cfe 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -64,7 +64,7 @@ func TestCreateSSL(t *testing.T) { Signature: "SHA256WithRSA", Status: "active", BundleMethod: "ubiquitous", - GeoRestrictions: ZoneCustomSSLGeoRestrictions{Label: "us"}, + GeoRestrictions: &ZoneCustomSSLGeoRestrictions{Label: "us"}, ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", UploadedOn: uploadedOn, ModifiedOn: modifiedOn, @@ -266,7 +266,7 @@ func TestUpdateSSL(t *testing.T) { Signature: "SHA256WithRSA", Status: "active", BundleMethod: "ubiquitous", - GeoRestrictions: ZoneCustomSSLGeoRestrictions{Label: "us"}, + GeoRestrictions: &ZoneCustomSSLGeoRestrictions{Label: "us"}, ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", UploadedOn: uploadedOn, ModifiedOn: modifiedOn, diff --git a/stream.go b/stream.go index e12630495a..95edd6b5e7 100644 --- a/stream.go +++ b/stream.go @@ -101,7 +101,7 @@ type StreamUploadFromURLParameters struct { Creator string `json:"creator,omitempty"` ThumbnailTimestampPct float64 `json:"thumbnailTimestampPct,omitempty"` AllowedOrigins []string `json:"allowedOrigins,omitempty"` - RequiredSignedURLs bool `json:"requiredSignedURLs,omitempty"` + RequireSignedURLs bool `json:"requireSignedURLs,omitempty"` Watermark UploadVideoURLWatermark `json:"watermark,omitempty"` } @@ -113,7 +113,7 @@ type StreamCreateVideoParameters struct { Creator string `json:"creator,omitempty"` ThumbnailTimestampPct float64 `json:"thumbnailTimestampPct,omitempty"` AllowedOrigins []string `json:"allowedOrigins,omitempty"` - RequiredSignedURLs bool `json:"requiredSignedURLs,omitempty"` + RequireSignedURLs bool `json:"requireSignedURLs,omitempty"` Watermark UploadVideoURLWatermark `json:"watermark,omitempty"` } diff --git a/teams_accounts.go b/teams_accounts.go index f90707147c..7667a155be 100644 --- a/teams_accounts.go +++ b/teams_accounts.go @@ -97,8 +97,9 @@ type TeamsLoggingSettings struct { } type TeamsDeviceSettings struct { - GatewayProxyEnabled bool `json:"gateway_proxy_enabled"` - GatewayProxyUDPEnabled bool `json:"gateway_udp_proxy_enabled"` + GatewayProxyEnabled bool `json:"gateway_proxy_enabled"` + GatewayProxyUDPEnabled bool `json:"gateway_udp_proxy_enabled"` + RootCertificateInstallationEnabled bool `json:"root_certificate_installation_enabled"` } type TeamsDeviceSettingsResponse struct { diff --git a/teams_accounts_test.go b/teams_accounts_test.go index a9535528bf..1741217355 100644 --- a/teams_accounts_test.go +++ b/teams_accounts_test.go @@ -239,7 +239,7 @@ func TestTeamsAccountGetDeviceConfiguration(t *testing.T) { "success": true, "errors": [], "messages": [], - "result": {"gateway_proxy_enabled": true,"gateway_udp_proxy_enabled":false} + "result": {"gateway_proxy_enabled": true,"gateway_udp_proxy_enabled":false, "root_certificate_installation_enabled":true} }`) } @@ -249,8 +249,9 @@ func TestTeamsAccountGetDeviceConfiguration(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, actual, TeamsDeviceSettings{ - GatewayProxyEnabled: true, - GatewayProxyUDPEnabled: false, + GatewayProxyEnabled: true, + GatewayProxyUDPEnabled: false, + RootCertificateInstallationEnabled: true, }) } } @@ -266,21 +267,23 @@ func TestTeamsAccountUpdateDeviceConfiguration(t *testing.T) { "success": true, "errors": [], "messages": [], - "result": {"gateway_proxy_enabled": true,"gateway_udp_proxy_enabled":true} + "result": {"gateway_proxy_enabled": true,"gateway_udp_proxy_enabled":true, "root_certificate_installation_enabled":true} }`) } mux.HandleFunc("/accounts/"+testAccountID+"/devices/settings", handler) actual, err := client.TeamsAccountDeviceUpdateConfiguration(context.Background(), testAccountID, TeamsDeviceSettings{ - GatewayProxyUDPEnabled: true, - GatewayProxyEnabled: true, + GatewayProxyUDPEnabled: true, + GatewayProxyEnabled: true, + RootCertificateInstallationEnabled: true, }) if assert.NoError(t, err) { assert.Equal(t, actual, TeamsDeviceSettings{ - GatewayProxyEnabled: true, - GatewayProxyUDPEnabled: true, + GatewayProxyEnabled: true, + GatewayProxyUDPEnabled: true, + RootCertificateInstallationEnabled: true, }) } } diff --git a/teams_list.go b/teams_list.go index 2f43001569..30c21d9d2d 100644 --- a/teams_list.go +++ b/teams_list.go @@ -172,6 +172,7 @@ func (api *API) ListTeamsListItems(ctx context.Context, rc *ResourceContainer, p var teamListItems []TeamsListItem var lResponse TeamsListItemsListResponse for { + lResponse = TeamsListItemsListResponse{} uri := buildURI( fmt.Sprintf("/%s/%s/gateway/lists/%s/items", rc.Level, rc.Identifier, params.ListID), params, diff --git a/teams_rules.go b/teams_rules.go index 5a7732049b..d177e2c706 100644 --- a/teams_rules.go +++ b/teams_rules.go @@ -35,6 +35,49 @@ type TeamsRuleSettings struct { // whether to disable dnssec validation for allow action InsecureDisableDNSSECValidation bool `json:"insecure_disable_dnssec_validation"` + + // settings for rules with egress action + EgressSettings *EgressSettings `json:"egress"` + + // DLP payload logging configuration + PayloadLog *TeamsDlpPayloadLogSettings `json:"payload_log"` + + //AuditSsh Settings + AuditSSH *AuditSSHRuleSettings `json:"audit_ssh"` + + // Turns on ip category based filter on dns if the rule contains dns category checks + IPCategories bool `json:"ip_categories"` + + // Allow parent MSP accounts to enable bypass their children's rules. Do not set them for non MSP accounts. + AllowChildBypass *bool `json:"allow_child_bypass,omitempty"` + + // Allow child MSP accounts to bypass their parent's rules. Do not set them for non MSP accounts. + BypassParentRule *bool `json:"bypass_parent_rule,omitempty"` + + // Action taken when an untrusted origin certificate error occurs in a http allow rule + UntrustedCertSettings *UntrustedCertSettings `json:"untrusted_cert"` +} + +type TeamsGatewayUntrustedCertAction string + +const ( + UntrustedCertPassthrough TeamsGatewayUntrustedCertAction = "pass_through" + UntrustedCertBlock TeamsGatewayUntrustedCertAction = "block" + UntrustedCertError TeamsGatewayUntrustedCertAction = "error" +) + +type UntrustedCertSettings struct { + Action TeamsGatewayUntrustedCertAction `json:"action"` +} + +type AuditSSHRuleSettings struct { + CommandLogging bool `json:"command_logging"` +} + +type EgressSettings struct { + Ipv6Range string `json:"ipv6"` + Ipv4 string `json:"ipv4"` + Ipv4Fallback string `json:"ipv4_fallback"` } // TeamsL4OverrideSettings used in l4 filter type rule with action set to override. @@ -44,11 +87,12 @@ type TeamsL4OverrideSettings struct { } type TeamsBISOAdminControlSettings struct { - DisablePrinting bool `json:"dp"` - DisableCopyPaste bool `json:"dcp"` - DisableDownload bool `json:"dd"` - DisableUpload bool `json:"du"` - DisableKeyboard bool `json:"dk"` + DisablePrinting bool `json:"dp"` + DisableCopyPaste bool `json:"dcp"` + DisableDownload bool `json:"dd"` + DisableUpload bool `json:"du"` + DisableKeyboard bool `json:"dk"` + DisableClipboardRedirection bool `json:"dcr"` } type TeamsCheckSessionSettings struct { @@ -56,29 +100,36 @@ type TeamsCheckSessionSettings struct { Duration Duration `json:"duration"` } +type TeamsDlpPayloadLogSettings struct { + Enabled bool `json:"enabled"` +} + type TeamsFilterType string type TeamsGatewayAction string const ( - HttpFilter TeamsFilterType = "http" - DnsFilter TeamsFilterType = "dns" - L4Filter TeamsFilterType = "l4" + HttpFilter TeamsFilterType = "http" + DnsFilter TeamsFilterType = "dns" + L4Filter TeamsFilterType = "l4" + EgressFilter TeamsFilterType = "egress" ) const ( - Allow TeamsGatewayAction = "allow" - Block TeamsGatewayAction = "block" - SafeSearch TeamsGatewayAction = "safesearch" - YTRestricted TeamsGatewayAction = "ytrestricted" - On TeamsGatewayAction = "on" - Off TeamsGatewayAction = "off" - Scan TeamsGatewayAction = "scan" - NoScan TeamsGatewayAction = "noscan" - Isolate TeamsGatewayAction = "isolate" - NoIsolate TeamsGatewayAction = "noisolate" - Override TeamsGatewayAction = "override" - L4Override TeamsGatewayAction = "l4_override" + Allow TeamsGatewayAction = "allow" // dns|http|l4 + Block TeamsGatewayAction = "block" // dns|http|l4 + SafeSearch TeamsGatewayAction = "safesearch" // dns + YTRestricted TeamsGatewayAction = "ytrestricted" // dns + On TeamsGatewayAction = "on" // http + Off TeamsGatewayAction = "off" // http + Scan TeamsGatewayAction = "scan" // http + NoScan TeamsGatewayAction = "noscan" // http + Isolate TeamsGatewayAction = "isolate" // http + NoIsolate TeamsGatewayAction = "noisolate" // http + Override TeamsGatewayAction = "override" // http + L4Override TeamsGatewayAction = "l4_override" // l4 + Egress TeamsGatewayAction = "egress" // egress + AuditSSH TeamsGatewayAction = "audit_ssh" // l4 ) func TeamsRulesActionValues() []string { @@ -95,6 +146,16 @@ func TeamsRulesActionValues() []string { string(NoIsolate), string(Override), string(L4Override), + string(Egress), + string(AuditSSH), + } +} + +func TeamsRulesUntrustedCertActionValues() []string { + return []string{ + string(UntrustedCertPassthrough), + string(UntrustedCertBlock), + string(UntrustedCertError), } } diff --git a/teams_rules_test.go b/teams_rules_test.go index e86642b2e0..0378d5bcf1 100644 --- a/teams_rules_test.go +++ b/teams_rules_test.go @@ -50,7 +50,10 @@ func TestTeamsRules(t *testing.T) { "enforce": true, "duration": "15m0s" }, - "insecure_disable_dnssec_validation": false + "insecure_disable_dnssec_validation": false, + "untrusted_cert": { + "action": "error" + } } }, { @@ -78,7 +81,10 @@ func TestTeamsRules(t *testing.T) { "biso_admin_controls": null, "add_headers": null, "check_session": null, - "insecure_disable_dnssec_validation": true + "insecure_disable_dnssec_validation": true, + "untrusted_cert": { + "action": "pass_through" + } } } ] @@ -114,6 +120,9 @@ func TestTeamsRules(t *testing.T) { Duration: Duration{900 * time.Second}, }, InsecureDisableDNSSECValidation: false, + UntrustedCertSettings: &UntrustedCertSettings{ + Action: UntrustedCertError, + }, }, CreatedAt: &createdAt, UpdatedAt: &updatedAt, @@ -142,6 +151,9 @@ func TestTeamsRules(t *testing.T) { CheckSession: nil, // setting is invalid for block rules, just testing serialization here InsecureDisableDNSSECValidation: true, + UntrustedCertSettings: &UntrustedCertSettings{ + Action: UntrustedCertPassthrough, + }, }, CreatedAt: &createdAt, UpdatedAt: &updatedAt, @@ -196,7 +208,10 @@ func TestTeamsRule(t *testing.T) { "enforce": true, "duration": "15m0s" }, - "insecure_disable_dnssec_validation": false + "insecure_disable_dnssec_validation": false, + "untrusted_cert": { + "action": "block" + } } } } @@ -231,6 +246,9 @@ func TestTeamsRule(t *testing.T) { Duration: Duration{900 * time.Second}, }, InsecureDisableDNSSECValidation: false, + UntrustedCertSettings: &UntrustedCertSettings{ + Action: UntrustedCertBlock, + }, }, CreatedAt: &createdAt, UpdatedAt: &updatedAt, @@ -246,7 +264,7 @@ func TestTeamsRule(t *testing.T) { } } -func TestTeamsCreateRule(t *testing.T) { +func TestTeamsCreateHTTPRule(t *testing.T) { setup() defer teardown() @@ -270,11 +288,7 @@ func TestTeamsCreateRule(t *testing.T) { "identity": "", "rule_settings": { "block_page_enabled": false, - "block_reason": "", - "override_ips": null, - "override_host": "", - "l4override": null, - "biso_admin_controls": null, + "biso_admin_controls": {"dp": true, "du": true, "dk": true}, "add_headers": { "X-Test": ["abcd"] }, @@ -300,18 +314,162 @@ func TestTeamsCreateRule(t *testing.T) { Identity: "", DevicePosture: "", RuleSettings: TeamsRuleSettings{ - BlockPageEnabled: false, - BlockReason: "", - OverrideIPs: nil, - OverrideHost: "", - L4Override: nil, - AddHeaders: http.Header{"X-Test": []string{"abcd"}}, - BISOAdminControls: nil, + BlockPageEnabled: false, + BlockReason: "", + OverrideIPs: nil, + OverrideHost: "", + L4Override: nil, + AddHeaders: http.Header{"X-Test": []string{"abcd"}}, + BISOAdminControls: &TeamsBISOAdminControlSettings{ + DisablePrinting: true, + DisableKeyboard: true, + DisableUpload: true, + }, CheckSession: &TeamsCheckSessionSettings{ Enforce: true, Duration: Duration{300 * time.Second}, }, InsecureDisableDNSSECValidation: false, + EgressSettings: nil, + }, + DeletedAt: nil, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/gateway/rules", handler) + + actual, err := client.TeamsCreateRule(context.Background(), testAccountID, want) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestTeamsCreateEgressRule(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "name": "egress via chicago", + "description": "rule description", + "precedence": 1000, + "enabled": false, + "action": "egress", + "filters": [ + "egress" + ], + "traffic": "net.src.geo.country == \"US\"", + "identity": "", + "rule_settings": { + "egress": { + "ipv6": "2a06:98c1:54::c61/64", + "ipv4": "2.2.2.2", + "ipv4_fallback": "1.1.1.1" + } + } + } + } + `) + } + + want := TeamsRule{ + Name: "egress via chicago", + Description: "rule description", + Precedence: 1000, + Enabled: false, + Action: Egress, + Filters: []TeamsFilterType{EgressFilter}, + Traffic: `net.src.geo.country == "US"`, + Identity: "", + DevicePosture: "", + RuleSettings: TeamsRuleSettings{ + BlockPageEnabled: false, + BlockReason: "", + OverrideIPs: nil, + OverrideHost: "", + L4Override: nil, + AddHeaders: nil, + BISOAdminControls: nil, + CheckSession: nil, + InsecureDisableDNSSECValidation: false, + EgressSettings: &EgressSettings{ + Ipv6Range: "2a06:98c1:54::c61/64", + Ipv4: "2.2.2.2", + Ipv4Fallback: "1.1.1.1", + }, + }, + DeletedAt: nil, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/gateway/rules", handler) + + actual, err := client.TeamsCreateRule(context.Background(), testAccountID, want) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestTeamsCreateL4Rule(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "name": "block 4.4.4.4", + "description": "rule description", + "precedence": 1000, + "enabled": true, + "action": "audit_ssh", + "filters": [ + "l4" + ], + "traffic": "net.src.geo.country == \"US\"", + "identity": "", + "rule_settings": { + "audit_ssh": { "command_logging": true } + } + } + } + `) + } + + want := TeamsRule{ + Name: "block 4.4.4.4", + Description: "rule description", + Precedence: 1000, + Enabled: true, + Action: AuditSSH, + Filters: []TeamsFilterType{L4Filter}, + Traffic: `net.src.geo.country == "US"`, + Identity: "", + DevicePosture: "", + RuleSettings: TeamsRuleSettings{ + BlockPageEnabled: false, + BlockReason: "", + OverrideIPs: nil, + OverrideHost: "", + L4Override: nil, + AddHeaders: nil, + BISOAdminControls: nil, + CheckSession: nil, + InsecureDisableDNSSECValidation: false, + EgressSettings: nil, + AuditSSH: &AuditSSHRuleSettings{ + CommandLogging: true, + }, }, DeletedAt: nil, } diff --git a/testdata/fixtures/dns/list_page_1.json b/testdata/fixtures/dns/list_page_1.json new file mode 100644 index 0000000000..505577398b --- /dev/null +++ b/testdata/fixtures/dns/list_page_1.json @@ -0,0 +1,83 @@ +{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "372e67954025e0ba6aaa6d586b9e0b59", + "type": "A", + "name": "example.com", + "content": "198.51.100.4", + "proxiable": true, + "proxied": true, + "ttl": 120, + "locked": false, + "zone_id": "d56084adb405e0b7e32c52321bf07be6", + "zone_name": "example.com", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "data": {}, + "meta": { + "auto_added": true, + "source": "primary" + }, + "tags": [ + "tag1", + "tag2extended" + ] + }, + { + "id": "7eb0a9821aec4b1395bd8cc03d88c17d", + "type": "A", + "name": "sub1.example.com", + "content": "198.51.100.5", + "proxiable": true, + "proxied": false, + "ttl": 120, + "locked": false, + "zone_id": "d56084adb405e0b7e32c52321bf07be6", + "zone_name": "example.com", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "data": {}, + "meta": { + "auto_added": true, + "source": "primary" + }, + "tags": [ + "tag1", + "tag2extended" + ] + }, + { + "id": "4c2c40857e334a2d903dd28f65a99682", + "type": "A", + "name": "sub2.example.com", + "content": "198.51.100.6", + "proxiable": true, + "proxied": true, + "ttl": 120, + "locked": false, + "zone_id": "d56084adb405e0b7e32c52321bf07be6", + "zone_name": "example.com", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "data": {}, + "meta": { + "auto_added": true, + "source": "primary" + }, + "tags": [ + "tag1", + "tag2extended" + ] + } + ], + "result_info": { + "count": 3, + "page": 1, + "per_page": 3, + "total_count": 5, + "total_pages": 2 + } +} diff --git a/testdata/fixtures/dns/list_page_2.json b/testdata/fixtures/dns/list_page_2.json new file mode 100644 index 0000000000..bb3d8f7277 --- /dev/null +++ b/testdata/fixtures/dns/list_page_2.json @@ -0,0 +1,60 @@ +{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "97e1dc2d19204b448b6ee04724f005ba", + "type": "A", + "name": "sub3.example.com", + "content": "198.51.100.7", + "proxiable": true, + "proxied": false, + "ttl": 120, + "locked": false, + "zone_id": "d56084adb405e0b7e32c52321bf07be6", + "zone_name": "example.com", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "data": {}, + "meta": { + "auto_added": true, + "source": "primary" + }, + "tags": [ + "tag1", + "tag2extended" + ] + }, + { + "id": "5bafaa7059d3480da9f6e2ecd8468c33", + "type": "A", + "name": "sub4.example.com", + "content": "198.51.100.8", + "proxiable": true, + "proxied": false, + "ttl": 120, + "locked": false, + "zone_id": "d56084adb405e0b7e32c52321bf07be6", + "zone_name": "example.com", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "data": {}, + "meta": { + "auto_added": true, + "source": "primary" + }, + "tags": [ + "tag1", + "tag2extended" + ] + } + ], + "result_info": { + "count": 2, + "page": 2, + "per_page": 3, + "total_count": 5, + "total_pages": 2 + } +} diff --git a/testdata/fixtures/tunnel/multiple_full.json b/testdata/fixtures/tunnel/multiple_full.json index 409911a8f2..586dd9003a 100644 --- a/testdata/fixtures/tunnel/multiple_full.json +++ b/testdata/fixtures/tunnel/multiple_full.json @@ -20,5 +20,11 @@ } ] } - ] + ], + "result_info": { + "count": 1, + "page": 1, + "per_page": 20, + "total_count": 1 + } } diff --git a/testdata/fixtures/tunnel/single_full.json b/testdata/fixtures/tunnel/single_full.json index 5408bdb56e..e5178e650f 100644 --- a/testdata/fixtures/tunnel/single_full.json +++ b/testdata/fixtures/tunnel/single_full.json @@ -1,22 +1,25 @@ -{ - "success": true, - "errors": [], - "messages": [], - "result": { - "id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", - "name": "blog", - "created_at": "2009-11-10T23:00:00Z", - "deleted_at": "2009-11-10T23:00:00Z", - "connections": [ - { - "colo_name": "DFW", - "id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", - "is_pending_reconnect": false, - "client_id": "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", - "client_version": "2022.2.0", - "opened_at": "2021-01-25T18:22:34.317854Z", - "origin_ip": "198.51.100.1" - } - ] - } -} +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "name": "blog", + "created_at": "2009-11-10T23:00:00Z", + "deleted_at": "2009-11-10T23:00:00Z", + "connections": [ + { + "colo_name": "DFW", + "id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "is_pending_reconnect": false, + "client_id": "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", + "client_version": "2022.2.0", + "opened_at": "2021-01-25T18:22:34.317854Z", + "origin_ip": "198.51.100.1" + } + ], + "status": "healthy", + "tun_type": "cfd_tunnel", + "remote_config": true + } +} diff --git a/tiered_cache.go b/tiered_cache.go new file mode 100644 index 0000000000..77b0ce5bbd --- /dev/null +++ b/tiered_cache.go @@ -0,0 +1,316 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +type TieredCacheType int + +const ( + TieredCacheOff TieredCacheType = 0 + TieredCacheGeneric TieredCacheType = 1 + TieredCacheSmart TieredCacheType = 2 +) + +func (e TieredCacheType) String() string { + switch e { + case TieredCacheGeneric: + return "generic" + case TieredCacheSmart: + return "smart" + case TieredCacheOff: + return "off" + default: + return fmt.Sprintf("%d", int(e)) + } +} + +type TieredCache struct { + Type TieredCacheType + LastModified time.Time +} + +// GetTieredCache allows you to retrieve the current Tiered Cache Settings for a Zone. +// This function does not support custom topologies, only Generic and Smart Tiered Caching. +// +// API Reference: https://api.cloudflare.com/#smart-tiered-cache-get-smart-tiered-cache-setting +// API Reference: https://api.cloudflare.com/#tiered-cache-get-tiered-cache-setting +func (api *API) GetTieredCache(ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + var lastModified time.Time + + generic, err := getGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = generic.LastModified + + smart, err := getSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if smart.LastModified.After(lastModified) { + lastModified = smart.LastModified + } + + if generic.Type == TieredCacheOff { + return TieredCache{Type: TieredCacheOff, LastModified: lastModified}, nil + } + + if smart.Type == TieredCacheOff { + return TieredCache{Type: TieredCacheGeneric, LastModified: lastModified}, nil + } + + return TieredCache{Type: TieredCacheSmart, LastModified: lastModified}, nil +} + +// SetTieredCache allows you to set a zone's tiered cache topology between the available types. +// Using the value of TieredCacheOff will disable Tiered Cache entirely. +// +// API Reference: https://api.cloudflare.com/#smart-tiered-cache-patch-smart-tiered-cache-setting +// API Reference: https://api.cloudflare.com/#tiered-cache-patch-tiered-cache-setting +func (api *API) SetTieredCache(ctx context.Context, rc *ResourceContainer, value TieredCacheType) (TieredCache, error) { + if value == TieredCacheOff { + return api.DeleteTieredCache(ctx, rc) + } + + var lastModified time.Time + + if value == TieredCacheGeneric { + result, err := deleteSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = result.LastModified + + result, err = enableGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if result.LastModified.After(lastModified) { + lastModified = result.LastModified + } + return TieredCache{Type: TieredCacheGeneric, LastModified: lastModified}, nil + } + + result, err := enableGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = result.LastModified + + result, err = enableSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if result.LastModified.After(lastModified) { + lastModified = result.LastModified + } + return TieredCache{Type: TieredCacheSmart, LastModified: lastModified}, nil +} + +// DeleteTieredCache allows you to delete the tiered cache settings for a zone. +// This is equivalent to using SetTieredCache with the value of TieredCacheOff. +// +// API Reference: https://api.cloudflare.com/#smart-tiered-cache-delete-smart-tiered-cache-setting +// API Reference: https://api.cloudflare.com/#tiered-cache-patch-tiered-cache-setting +func (api *API) DeleteTieredCache(ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + var lastModified time.Time + + result, err := deleteSmartTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + lastModified = result.LastModified + + result, err = disableGenericTieredCache(api, ctx, rc) + if err != nil { + return TieredCache{}, err + } + + if result.LastModified.After(lastModified) { + lastModified = result.LastModified + } + return TieredCache{Type: TieredCacheOff, LastModified: lastModified}, nil +} + +type tieredCacheResult struct { + ID string `json:"id"` + Value string `json:"value,omitempty"` + LastModified time.Time `json:"modified_on"` +} + +type tieredCacheResponse struct { + Result tieredCacheResult `json:"result"` + Response +} + +type tieredCacheSetting struct { + Value string `json:"value"` +} + +func getGenericTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/argo/tiered_caching", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to retrieve generic tiered cache failed") + } + + if response.Result.Value == "off" { + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil + } + + return TieredCache{Type: TieredCacheGeneric, LastModified: response.Result.LastModified}, nil +} + +func getSmartTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/cache/tiered_cache_smart_topology_enable", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + var notFoundError *NotFoundError + if errors.As(err, ¬FoundError) { + return TieredCache{Type: TieredCacheOff}, nil + } + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to retrieve smart tiered cache failed") + } + + if response.Result.Value == "off" { + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil + } + return TieredCache{Type: TieredCacheSmart, LastModified: response.Result.LastModified}, nil +} + +func enableGenericTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/argo/tiered_caching", rc.Identifier) + setting := tieredCacheSetting{ + Value: "on", + } + body, err := json.Marshal(setting) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, body) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to enable generic tiered cache failed") + } + + return TieredCache{Type: TieredCacheGeneric, LastModified: response.Result.LastModified}, nil +} + +func enableSmartTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/cache/tiered_cache_smart_topology_enable", rc.Identifier) + setting := tieredCacheSetting{ + Value: "on", + } + body, err := json.Marshal(setting) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, body) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to enable smart tiered cache failed") + } + + return TieredCache{Type: TieredCacheSmart, LastModified: response.Result.LastModified}, nil +} + +func disableGenericTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/argo/tiered_caching", rc.Identifier) + setting := tieredCacheSetting{ + Value: "off", + } + body, err := json.Marshal(setting) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, body) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to disable generic tiered cache failed") + } + + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil +} + +func deleteSmartTieredCache(api *API, ctx context.Context, rc *ResourceContainer) (TieredCache, error) { + uri := fmt.Sprintf("/zones/%s/cache/tiered_cache_smart_topology_enable", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + var notFoundError *NotFoundError + if errors.As(err, ¬FoundError) { + return TieredCache{Type: TieredCacheOff}, nil + } + return TieredCache{Type: TieredCacheOff}, err + } + + var response tieredCacheResponse + err = json.Unmarshal(res, &response) + if err != nil { + return TieredCache{Type: TieredCacheOff}, err + } + + if !response.Success { + return TieredCache{Type: TieredCacheOff}, errors.New("request to disable smart tiered cache failed") + } + + return TieredCache{Type: TieredCacheOff, LastModified: response.Result.LastModified}, nil +} diff --git a/tiered_cache_test.go b/tiered_cache_test.go new file mode 100644 index 0000000000..c30ada6404 --- /dev/null +++ b/tiered_cache_test.go @@ -0,0 +1,321 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func createSmartTieredCacheHandler(val string, lastModified string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "editable": true, + "id": "tiered_cache_smart_topology_enable", + "modified_on": "%s", + "value": "%s" + } + }`, lastModified, val) + } +} + +func nonexistentSmartTieredCacheHandler() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(404) + fmt.Fprintf(w, `{ + "result": null, + "success": false, + "errors": [ + { + "code": 1142, + "message": "Unable to retrieve tiered_cache_smart_topology_enable setting value. The zone setting does not exist." + } + ], + "messages": [] + }`) + } +} + +func createGenericTieredCacheHandler(val string, lastModified string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "tiered_caching", + "value": "%s", + "modified_on": "%s", + "editable": false + } + }`, val, lastModified) + } +} + +func TestGetTieredCache(t *testing.T) { + t.Run("can identify when Smart Tiered Cache", func(t *testing.T) { + t.Run("is disabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("off", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("is enabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("zone setting does not exist", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) + + t.Run("can identify when generic tiered cache", func(t *testing.T) { + t.Run("is disabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("off", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheOff, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("is enabled", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) + + t.Run("determines the latest last modified when", func(t *testing.T) { + t.Run("smart tiered cache zone setting does not exist", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("generic tiered cache was modified more recently", func(t *testing.T) { + setup() + defer teardown() + + earlier := time.Now().Add(time.Minute * -5).Format(time.RFC3339) + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", earlier)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("smart tiered cache was modified more recently", func(t *testing.T) { + setup() + defer teardown() + + earlier := time.Now().Add(time.Minute * -5).Format(time.RFC3339) + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", earlier)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) +} + +func TestSetTieredCache(t *testing.T) { + t.Run("can enable tiered caching", func(t *testing.T) { + t.Run("using smart caching", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", createSmartTieredCacheHandler("on", lastModified)) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheSmart, + LastModified: wanted, + } + + got, err := client.SetTieredCache(context.Background(), ZoneIdentifier(testZoneID), TieredCacheSmart) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + + t.Run("use generic caching", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("on", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheGeneric, + LastModified: wanted, + } + + got, err := client.SetTieredCache(context.Background(), ZoneIdentifier(testZoneID), TieredCacheGeneric) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) + }) +} + +func TestDeleteTieredCache(t *testing.T) { + t.Run("can disable tiered caching", func(t *testing.T) { + setup() + defer teardown() + + lastModified := time.Now().Format(time.RFC3339) + + mux.HandleFunc("/zones/"+testZoneID+"/argo/tiered_caching", createGenericTieredCacheHandler("off", lastModified)) + mux.HandleFunc("/zones/"+testZoneID+"/cache/tiered_cache_smart_topology_enable", nonexistentSmartTieredCacheHandler()) + + wanted, _ := time.Parse(time.RFC3339, lastModified) + want := TieredCache{ + Type: TieredCacheOff, + LastModified: wanted, + } + + got, err := client.GetTieredCache(context.Background(), ZoneIdentifier(testZoneID)) + + if assert.NoError(t, err) { + assert.Equal(t, want, got) + } + }) +} diff --git a/tunnel.go b/tunnel.go index a2273d1598..702be8fbc8 100644 --- a/tunnel.go +++ b/tunnel.go @@ -23,16 +23,20 @@ type Tunnel struct { Connections []TunnelConnection `json:"connections,omitempty"` ConnsActiveAt *time.Time `json:"conns_active_at,omitempty"` ConnInactiveAt *time.Time `json:"conns_inactive_at,omitempty"` + TunnelType string `json:"tun_type,omitempty"` + Status string `json:"status,omitempty"` + RemoteConfig bool `json:"remote_config,omitempty"` } // Connection is the struct definition of a connection. type Connection struct { - ID string `json:"id,omitempty"` - Features []string `json:"features,omitempty"` - Version string `json:"version,omitempty"` - Arch string `json:"arch,omitempty"` - Connections []TunnelConnection `json:"conns,omitempty"` - RunAt *time.Time `json:"run_at,omitempty"` + ID string `json:"id,omitempty"` + Features []string `json:"features,omitempty"` + Version string `json:"version,omitempty"` + Arch string `json:"arch,omitempty"` + Connections []TunnelConnection `json:"conns,omitempty"` + RunAt *time.Time `json:"run_at,omitempty"` + ConfigVersion int `json:"config_version,omitempty"` } // TunnelConnection represents the connections associated with a tunnel. @@ -51,8 +55,12 @@ type TunnelConnection struct { type TunnelsDetailResponse struct { Result []Tunnel `json:"result"` Response + ResultInfo `json:"result_info"` } +// listTunnelsDefaultPageSize represents the default per_page size of the API. +var listTunnelsDefaultPageSize int = 100 + // TunnelDetailResponse is used for representing the API response payload for // a single tunnel. type TunnelDetailResponse struct { @@ -87,8 +95,9 @@ type TunnelTokenResponse struct { } type TunnelCreateParams struct { - Name string `json:"name,omitempty"` - Secret string `json:"tunnel_secret,omitempty"` + Name string `json:"name,omitempty"` + Secret string `json:"tunnel_secret,omitempty"` + ConfigSrc string `json:"config_src,omitempty"` } type TunnelUpdateParams struct { @@ -172,35 +181,60 @@ type TunnelListParams struct { UUID string `url:"uuid,omitempty"` // the tunnel ID IsDeleted *bool `url:"is_deleted,omitempty"` ExistedAt *time.Time `url:"existed_at,omitempty"` + + ResultInfo } -// Tunnels lists all tunnels. +// ListTunnels lists all tunnels. // // API reference: https://api.cloudflare.com/#cloudflare-tunnel-list-cloudflare-tunnels -func (api *API) Tunnels(ctx context.Context, rc *ResourceContainer, params TunnelListParams) ([]Tunnel, error) { +func (api *API) ListTunnels(ctx context.Context, rc *ResourceContainer, params TunnelListParams) ([]Tunnel, *ResultInfo, error) { if rc.Identifier == "" { - return []Tunnel{}, ErrMissingAccountID + return []Tunnel{}, &ResultInfo{}, ErrMissingAccountID } - uri := buildURI(fmt.Sprintf("/accounts/%s/cfd_tunnel", rc.Identifier), params) + autoPaginate := true + if params.PerPage >= 1 || params.Page >= 1 { + autoPaginate = false + } - res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) - if err != nil { - return []Tunnel{}, err + if params.PerPage < 1 { + params.PerPage = listTunnelsDefaultPageSize } - var argoDetailsResponse TunnelsDetailResponse - err = json.Unmarshal(res, &argoDetailsResponse) - if err != nil { - return []Tunnel{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + if params.Page < 1 { + params.Page = 1 } - return argoDetailsResponse.Result, nil + + var records []Tunnel + var listResponse TunnelsDetailResponse + + for { + uri := buildURI(fmt.Sprintf("/accounts/%s/cfd_tunnel", rc.Identifier), params) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []Tunnel{}, &ResultInfo{}, err + } + + err = json.Unmarshal(res, &listResponse) + if err != nil { + return []Tunnel{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + records = append(records, listResponse.Result...) + params.ResultInfo = listResponse.ResultInfo.Next() + if params.ResultInfo.Done() || !autoPaginate { + break + } + } + + return records, &listResponse.ResultInfo, nil } -// Tunnel returns a single Argo tunnel. +// GetTunnel returns a single Argo tunnel. // // API reference: https://api.cloudflare.com/#cloudflare-tunnel-get-cloudflare-tunnel -func (api *API) Tunnel(ctx context.Context, rc *ResourceContainer, tunnelID string) (Tunnel, error) { +func (api *API) GetTunnel(ctx context.Context, rc *ResourceContainer, tunnelID string) (Tunnel, error) { if rc.Identifier == "" { return Tunnel{}, ErrMissingAccountID } @@ -242,9 +276,7 @@ func (api *API) CreateTunnel(ctx context.Context, rc *ResourceContainer, params uri := fmt.Sprintf("/accounts/%s/cfd_tunnel", rc.Identifier) - tunnel := Tunnel{Name: params.Name, Secret: params.Secret} - - res, err := api.makeRequestContext(ctx, http.MethodPost, uri, tunnel) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) if err != nil { return Tunnel{}, err } @@ -358,10 +390,10 @@ func (api *API) GetTunnelConfiguration(ctx context.Context, rc *ResourceContaine return tunnelDetails, nil } -// TunnelConnections gets all connections on a tunnel. +// ListTunnelConnections gets all connections on a tunnel. // // API reference: https://api.cloudflare.com/#cloudflare-tunnel-list-cloudflare-tunnel-connections -func (api *API) TunnelConnections(ctx context.Context, rc *ResourceContainer, tunnelID string) ([]Connection, error) { +func (api *API) ListTunnelConnections(ctx context.Context, rc *ResourceContainer, tunnelID string) ([]Connection, error) { if rc.Identifier == "" { return []Connection{}, ErrMissingAccountID } @@ -433,10 +465,10 @@ func (api *API) CleanupTunnelConnections(ctx context.Context, rc *ResourceContai return nil } -// TunnelToken that allows to run a tunnel. +// GetTunnelToken that allows to run a tunnel. // // API reference: https://api.cloudflare.com/#cloudflare-tunnel-get-cloudflare-tunnel-token -func (api *API) TunnelToken(ctx context.Context, rc *ResourceContainer, tunnelID string) (string, error) { +func (api *API) GetTunnelToken(ctx context.Context, rc *ResourceContainer, tunnelID string) (string, error) { if rc.Identifier == "" { return "", ErrMissingAccountID } diff --git a/tunnel_test.go b/tunnel_test.go index 8cfb50fc75..cacc12fb60 100644 --- a/tunnel_test.go +++ b/tunnel_test.go @@ -4,13 +4,14 @@ import ( "context" "fmt" "net/http" + "net/url" "testing" "time" "github.com/stretchr/testify/assert" ) -func TestTunnels(t *testing.T) { +func TestListTunnels(t *testing.T) { setup() defer teardown() @@ -25,13 +26,13 @@ func TestTunnels(t *testing.T) { createdAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") deletedAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") want := []Tunnel{{ - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, Name: "blog", CreatedAt: &createdAt, DeletedAt: &deletedAt, Connections: []TunnelConnection{{ ColoName: "DFW", - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, IsPendingReconnect: false, ClientID: "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", ClientVersion: "2022.2.0", @@ -40,14 +41,64 @@ func TestTunnels(t *testing.T) { }}, }} - actual, err := client.Tunnels(context.Background(), AccountIdentifier(testAccountID), TunnelListParams{UUID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415"}) + actual, _, err := client.ListTunnels(context.Background(), AccountIdentifier(testAccountID), TunnelListParams{UUID: testTunnelID}) if assert.NoError(t, err) { assert.Equal(t, want, actual) } } -func TestTunnel(t *testing.T) { +func TestListTunnelsPagination(t *testing.T) { + setup() + defer teardown() + + handler := 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") + qry, _ := url.Parse(r.RequestURI) + assert.Equal(t, "blog", qry.Query().Get("name")) + assert.Equal(t, "2", qry.Query().Get("page")) + assert.Equal(t, "1", qry.Query().Get("per_page")) + fmt.Fprint(w, loadFixture("tunnel", "multiple_full")) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel", handler) + + createdAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") + deletedAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") + want := []Tunnel{ + { + ID: testTunnelID, + Name: "blog", + CreatedAt: &createdAt, + DeletedAt: &deletedAt, + Connections: []TunnelConnection{{ + ColoName: "DFW", + ID: testTunnelID, + IsPendingReconnect: false, + ClientID: "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", + ClientVersion: "2022.2.0", + OpenedAt: "2021-01-25T18:22:34.317854Z", + OriginIP: "198.51.100.1", + }}, + }, + } + + actual, _, err := client.ListTunnels(context.Background(), AccountIdentifier(testAccountID), + TunnelListParams{ + Name: "blog", + ResultInfo: ResultInfo{ + Page: 2, + PerPage: 1, + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetTunnel(t *testing.T) { setup() defer teardown() @@ -57,27 +108,30 @@ func TestTunnel(t *testing.T) { fmt.Fprint(w, loadFixture("tunnel", "single_full")) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/"+testTunnelID, handler) createdAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") deletedAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") want := Tunnel{ - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, Name: "blog", CreatedAt: &createdAt, DeletedAt: &deletedAt, Connections: []TunnelConnection{{ ColoName: "DFW", - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, IsPendingReconnect: false, ClientID: "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", ClientVersion: "2022.2.0", OpenedAt: "2021-01-25T18:22:34.317854Z", OriginIP: "198.51.100.1", }}, + TunnelType: "cfd_tunnel", + Status: "healthy", + RemoteConfig: true, } - actual, err := client.Tunnel(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + actual, err := client.GetTunnel(context.Background(), AccountIdentifier(testAccountID), testTunnelID) if assert.NoError(t, err) { assert.Equal(t, want, actual) @@ -99,22 +153,25 @@ func TestCreateTunnel(t *testing.T) { createdAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") deletedAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") want := Tunnel{ - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, Name: "blog", CreatedAt: &createdAt, DeletedAt: &deletedAt, Connections: []TunnelConnection{{ ColoName: "DFW", - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, IsPendingReconnect: false, ClientID: "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", ClientVersion: "2022.2.0", OpenedAt: "2021-01-25T18:22:34.317854Z", OriginIP: "198.51.100.1", }}, + TunnelType: "cfd_tunnel", + Status: "healthy", + RemoteConfig: true, } - actual, err := client.CreateTunnel(context.Background(), AccountIdentifier(testAccountID), TunnelCreateParams{Name: "blog", Secret: "notarealsecret"}) + actual, err := client.CreateTunnel(context.Background(), AccountIdentifier(testAccountID), TunnelCreateParams{Name: "blog", Secret: "notarealsecret", ConfigSrc: "cloudflare"}) if assert.NoError(t, err) { assert.Equal(t, want, actual) @@ -132,9 +189,9 @@ func TestUpdateTunnelConfiguration(t *testing.T) { fmt.Fprint(w, loadFixture("tunnel", "configuration")) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/configurations", handler) + mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/configurations", testAccountID, testTunnelID), handler) want := TunnelConfigurationResult{ - TunnelID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + TunnelID: testTunnelID, Version: 5, Config: TunnelConfiguration{ Ingress: []UnvalidatedIngressRule{ @@ -158,7 +215,7 @@ func TestUpdateTunnelConfiguration(t *testing.T) { }} actual, err := client.UpdateTunnelConfiguration(context.Background(), AccountIdentifier(testAccountID), TunnelConfigurationParams{ - TunnelID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + TunnelID: testTunnelID, Config: TunnelConfiguration{ Ingress: []UnvalidatedIngressRule{ { @@ -197,9 +254,9 @@ func TestGetTunnelConfiguration(t *testing.T) { fmt.Fprint(w, loadFixture("tunnel", "configuration")) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/configurations", handler) + mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/configurations", testAccountID, testTunnelID), handler) want := TunnelConfigurationResult{ - TunnelID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + TunnelID: testTunnelID, Version: 5, Config: TunnelConfiguration{ Ingress: []UnvalidatedIngressRule{ @@ -222,7 +279,7 @@ func TestGetTunnelConfiguration(t *testing.T) { }, }} - actual, err := client.GetTunnelConfiguration(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + actual, err := client.GetTunnelConfiguration(context.Background(), AccountIdentifier(testAccountID), testTunnelID) if assert.NoError(t, err) { assert.Equal(t, want, actual) @@ -249,6 +306,7 @@ func TestTunnelConnections(t *testing.T) { ], "version": "2022.2.0", "arch": "linux_amd64", + "config_version": 15, "run_at":"2009-11-10T23:00:00Z", "conns": [ { @@ -266,7 +324,7 @@ func TestTunnelConnections(t *testing.T) { `) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/connections", handler) + mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/connections", testAccountID, testTunnelID), handler) runAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z") want := []Connection{ @@ -282,17 +340,18 @@ func TestTunnelConnections(t *testing.T) { RunAt: &runAt, Connections: []TunnelConnection{{ ColoName: "DFW", - ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + ID: testTunnelID, IsPendingReconnect: false, ClientID: "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90", ClientVersion: "2022.2.0", OpenedAt: "2021-01-25T18:22:34.317854Z", OriginIP: "198.51.100.1", }}, + ConfigVersion: 15, }, } - actual, err := client.TunnelConnections(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + actual, err := client.ListTunnelConnections(context.Background(), AccountIdentifier(testAccountID), testTunnelID) if assert.NoError(t, err) { assert.Equal(t, want, actual) @@ -309,9 +368,9 @@ func TestDeleteTunnel(t *testing.T) { fmt.Fprint(w, loadFixture("tunnel", "single_full")) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler) + mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s", testAccountID, testTunnelID), handler) - err := client.DeleteTunnel(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + err := client.DeleteTunnel(context.Background(), AccountIdentifier(testAccountID), testTunnelID) assert.NoError(t, err) } @@ -325,9 +384,9 @@ func TestCleanupTunnelConnections(t *testing.T) { fmt.Fprint(w, loadFixture("tunnel", "empty")) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/connections", handler) + mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/connections", testAccountID, testTunnelID), handler) - err := client.CleanupTunnelConnections(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + err := client.CleanupTunnelConnections(context.Background(), AccountIdentifier(testAccountID), testTunnelID) assert.NoError(t, err) } @@ -341,9 +400,9 @@ func TestTunnelToken(t *testing.T) { fmt.Fprint(w, loadFixture("tunnel", "token")) } - mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/token", handler) + mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/token", testAccountID, testTunnelID), handler) - token, err := client.TunnelToken(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + token, err := client.GetTunnelToken(context.Background(), AccountIdentifier(testAccountID), testTunnelID) assert.NoError(t, err) assert.Equal(t, "ZHNraGdhc2RraGFza2hqZGFza2poZGFza2poYXNrZGpoYWtzamRoa2FzZGpoa2FzamRoa2Rhc2po\na2FzamRoa2FqCg==", token) } diff --git a/turnstile.go b/turnstile.go new file mode 100644 index 0000000000..84c0bdca5d --- /dev/null +++ b/turnstile.go @@ -0,0 +1,244 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +var ErrMissingSiteKey = errors.New("required site key missing") + +type TurnstileWidget struct { + SiteKey string `json:"sitekey,omitempty"` + Secret string `json:"secret,omitempty"` + CreatedOn *time.Time `json:"created_on,omitempty"` + ModifiedOn *time.Time `json:"modified_on,omitempty"` + Name string `json:"name,omitempty"` + Domains []string `json:"domains,omitempty"` + Mode string `json:"mode,omitempty"` + BotFightMode bool `json:"bot_fight_mode,omitempty"` + Region string `json:"region,omitempty"` + OffLabel bool `json:"offlabel,omitempty"` +} + +type CreateTurnstileWidgetParams struct { + Name string `json:"name,omitempty"` + Domains []string `json:"domains,omitempty"` + Mode string `json:"mode,omitempty"` + BotFightMode bool `json:"bot_fight_mode,omitempty"` + Region string `json:"region,omitempty"` + OffLabel bool `json:"offlabel,omitempty"` +} + +type UpdateTurnstileWidgetParams struct { + SiteKey string `json:"-"` + Name string `json:"name,omitempty"` + Domains []string `json:"domains,omitempty"` + Mode string `json:"mode,omitempty"` + BotFightMode bool `json:"bot_fight_mode,omitempty"` + Region string `json:"region,omitempty"` + OffLabel bool `json:"offlabel,omitempty"` +} + +type TurnstileWidgetResponse struct { + Response + Result TurnstileWidget `json:"result"` +} + +type ListTurnstileWidgetParams struct { + ResultInfo + Direction string `url:"direction,omitempty"` + Order OrderDirection `url:"order,omitempty"` +} + +type ListTurnstileWidgetResponse struct { + Response + ResultInfo `json:"result_info"` + Result []TurnstileWidget `json:"result"` +} + +type RotateTurnstileWidgetParams struct { + SiteKey string `json:"-"` + InvalidateImmediately bool `json:"invalidate_immediately,omitempty"` +} + +// CreateTurnstileWidget creates a new challenge widgets. +// +// API reference: https://api.cloudflare.com/#challenge-widgets-properties +func (api *API) CreateTurnstileWidget(ctx context.Context, rc *ResourceContainer, params CreateTurnstileWidgetParams) (TurnstileWidget, error) { + if rc.Identifier == "" { + return TurnstileWidget{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/challenges/widgets", rc.Identifier) + res, err := api.makeRequestContext(ctx, "POST", uri, params) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var r TurnstileWidgetResponse + err = json.Unmarshal(res, &r) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} + +// ListTurnstileWidgets lists challenge widgets. +// +// API reference: https://api.cloudflare.com/#challenge-widgets-list-challenge-widgets +func (api *API) ListTurnstileWidgets(ctx context.Context, rc *ResourceContainer, params ListTurnstileWidgetParams) ([]TurnstileWidget, *ResultInfo, error) { + if rc.Identifier == "" { + return []TurnstileWidget{}, &ResultInfo{}, ErrMissingAccountID + } + autoPaginate := true + if params.PerPage >= 1 || params.Page >= 1 { + autoPaginate = false + } + + if params.PerPage < 1 { + params.PerPage = 25 + } + + if params.Page < 1 { + params.Page = 1 + } + + var widgets []TurnstileWidget + var r ListTurnstileWidgetResponse + for { + uri := buildURI(fmt.Sprintf("/accounts/%s/challenges/widgets", rc.Identifier), params) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + + if err != nil { + return []TurnstileWidget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + err = json.Unmarshal(res, &r) + if err != nil { + return []TurnstileWidget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + widgets = append(widgets, r.Result...) + params.ResultInfo = r.ResultInfo.Next() + if params.ResultInfo.Done() || !autoPaginate { + break + } + } + + return widgets, &r.ResultInfo, nil +} + +// GetTurnstileWidget shows a single challenge widget configuration. +// +// API reference: https://api.cloudflare.com/#challenge-widgets-challenge-widget-details +func (api *API) GetTurnstileWidget(ctx context.Context, rc *ResourceContainer, siteKey string) (TurnstileWidget, error) { + if rc.Identifier == "" { + return TurnstileWidget{}, ErrMissingAccountID + } + + if siteKey == "" { + return TurnstileWidget{}, ErrMissingSiteKey + } + + uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s", rc.Identifier, siteKey) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var r TurnstileWidgetResponse + err = json.Unmarshal(res, &r) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} + +// UpdateTurnstileWidget update the configuration of a widget. +// +// API reference: https://api.cloudflare.com/#challenge-widgets-update-a-challenge-widget +func (api *API) UpdateTurnstileWidget(ctx context.Context, rc *ResourceContainer, params UpdateTurnstileWidgetParams) (TurnstileWidget, error) { + if rc.Identifier == "" { + return TurnstileWidget{}, ErrMissingAccountID + } + + if params.SiteKey == "" { + return TurnstileWidget{}, ErrMissingSiteKey + } + + uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s", rc.Identifier, params.SiteKey) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var r TurnstileWidgetResponse + err = json.Unmarshal(res, &r) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +// RotateTurnstileWidget generates a new secret key for this widget. If +// invalidate_immediately is set to false, the previous secret remains valid for +// 2 hours. +// +// Note that secrets cannot be rotated again during the grace period. +// +// API reference: https://api.cloudflare.com/#challenge-widgets-rotate-secret-for-a-challenge-widget +func (api *API) RotateTurnstileWidget(ctx context.Context, rc *ResourceContainer, param RotateTurnstileWidgetParams) (TurnstileWidget, error) { + if rc.Identifier == "" { + return TurnstileWidget{}, ErrMissingAccountID + } + if param.SiteKey == "" { + return TurnstileWidget{}, ErrMissingSiteKey + } + + uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s/rotate_secret", rc.Identifier, param.SiteKey) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, param) + + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var r TurnstileWidgetResponse + err = json.Unmarshal(res, &r) + if err != nil { + return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} + +// DeleteTurnstileWidget delete a challenge widget. +// +// API reference: https://api.cloudflare.com/#challenge-widgets-delete-a-challenge-widget +func (api *API) DeleteTurnstileWidget(ctx context.Context, rc *ResourceContainer, siteKey string) error { + if rc.Identifier == "" { + return ErrMissingAccountID + } + + if siteKey == "" { + return ErrMissingSiteKey + } + uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s", rc.Identifier, siteKey) + + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var r TurnstileWidgetResponse + err = json.Unmarshal(res, &r) + if err != nil { + return fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return nil +} diff --git a/turnstile_test.go b/turnstile_test.go new file mode 100644 index 0000000000..f2193431f4 --- /dev/null +++ b/turnstile_test.go @@ -0,0 +1,334 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const testTurnstileWidgetSiteKey = "0x4AAF00AAAABn0R22HWm-YUc" + +var ( + turnstileWidgetCreatedOn, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.123123Z") + turnstileWidgetModifiedOn, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.123123Z") + expectedTurnstileWidget = TurnstileWidget{ + SiteKey: "0x4AAF00AAAABn0R22HWm-YUc", + Secret: "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + CreatedOn: &turnstileWidgetCreatedOn, + ModifiedOn: &turnstileWidgetModifiedOn, + Name: "blog.cloudflare.com login form", + Domains: []string{ + "203.0.113.1", + "cloudflare.com", + "blog.example.com", + }, + Mode: "invisible", + BotFightMode: true, + Region: "world", + OffLabel: false, + } +) + +func TestTurnstileWidget_Create(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets", 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, ` + { + "success": true, + "errors": [], + "messages": [], + "result": { + "sitekey": "0x4AAF00AAAABn0R22HWm-YUc", + "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + "created_on": "2014-01-01T05:20:00.123123Z", + "modified_on": "2014-01-01T05:20:00.123123Z", + "name": "blog.cloudflare.com login form", + "domains": [ + "203.0.113.1", + "cloudflare.com", + "blog.example.com" + ], + "mode": "invisible", + "bot_fight_mode": true, + "region": "world", + "offlabel": false + } + }`) + }) + + // Make sure missing account ID is thrown + _, err := client.CreateTurnstileWidget(context.Background(), AccountIdentifier(""), CreateTurnstileWidgetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + out, err := client.CreateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), CreateTurnstileWidgetParams{ + Name: "blog.cloudflare.com login form", + Mode: "invisible", + BotFightMode: true, + Domains: []string{ + "203.0.113.1", + "cloudflare.com", + "blog.example.com", + }, + Region: "world", + OffLabel: false, + }) + if assert.NoError(t, err) { + assert.Equal(t, expectedTurnstileWidget, out, "create challenge_widgets structs not equal") + } +} + +func TestTurnstileWidget_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + assert.Equal(t, "asc", r.URL.Query().Get("order")) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, ` +{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "sitekey": "0x4AAF00AAAABn0R22HWm-YUc", + "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + "created_on": "2014-01-01T05:20:00.123123Z", + "modified_on": "2014-01-01T05:20:00.123123Z", + "name": "blog.cloudflare.com login form", + "domains": [ + "203.0.113.1", + "cloudflare.com", + "blog.example.com" + ], + "mode": "invisible", + "bot_fight_mode": true, + "region": "world", + "offlabel": false + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } +}`) + }) + + _, _, err := client.ListTurnstileWidgets(context.Background(), AccountIdentifier(""), ListTurnstileWidgetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + out, results, err := client.ListTurnstileWidgets(context.Background(), AccountIdentifier(testAccountID), ListTurnstileWidgetParams{ + Order: OrderDirectionAsc, + }) + if assert.NoError(t, err) { + assert.Equal(t, 1, len(out), "expected 1 challenge_widgets") + assert.Equal(t, 20, results.PerPage, "expected 20 per page") + assert.Equal(t, expectedTurnstileWidget, out[0], "list challenge_widgets structs not equal") + } +} + +func TestTurnstileWidget_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey, 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, ` +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "sitekey": "0x4AAF00AAAABn0R22HWm-YUc", + "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + "created_on": "2014-01-01T05:20:00.123123Z", + "modified_on": "2014-01-01T05:20:00.123123Z", + "name": "blog.cloudflare.com login form", + "domains": [ + "203.0.113.1", + "cloudflare.com", + "blog.example.com" + ], + "mode": "invisible", + "bot_fight_mode": true, + "region": "world", + "offlabel": false + } +}`) + }) + + _, err := client.GetTurnstileWidget(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.GetTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingSiteKey, err) + } + + out, err := client.GetTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), testTurnstileWidgetSiteKey) + + if assert.NoError(t, err) { + assert.Equal(t, expectedTurnstileWidget, out, "get challenge_widgets structs not equal") + } +} + +func TestTurnstileWidgets_Update(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, ` +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "sitekey": "0x4AAF00AAAABn0R22HWm-YUc", + "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + "created_on": "2014-01-01T05:20:00.123123Z", + "modified_on": "2014-01-01T05:20:00.123123Z", + "name": "blog.cloudflare.com login form", + "domains": [ + "203.0.113.1", + "cloudflare.com", + "blog.example.com" + ], + "mode": "invisible", + "bot_fight_mode": true, + "region": "world", + "offlabel": false + } +}`) + }) + + _, err := client.UpdateTurnstileWidget(context.Background(), AccountIdentifier(""), UpdateTurnstileWidgetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.UpdateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), UpdateTurnstileWidgetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingSiteKey, err) + } + + out, err := client.UpdateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), UpdateTurnstileWidgetParams{ + SiteKey: testTurnstileWidgetSiteKey, + }) + if assert.NoError(t, err) { + assert.Equal(t, expectedTurnstileWidget, out, "update challenge_widgets structs not equal") + } +} + +func TestTurnstileWidgets_RotateSecret(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey+"/rotate_secret", 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, ` +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "sitekey": "0x4AAF00AAAABn0R22HWm-YUc", + "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + "created_on": "2014-01-01T05:20:00.123123Z", + "modified_on": "2014-01-01T05:20:00.123123Z", + "name": "blog.cloudflare.com login form", + "domains": [ + "203.0.113.1", + "cloudflare.com", + "blog.example.com" + ], + "mode": "invisible", + "bot_fight_mode": true, + "region": "world", + "offlabel": false + } +}`) + }) + + // Make sure missing account ID is thrown + _, err := client.RotateTurnstileWidget(context.Background(), AccountIdentifier(""), RotateTurnstileWidgetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.RotateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), RotateTurnstileWidgetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingSiteKey, err) + } + + out, err := client.RotateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), RotateTurnstileWidgetParams{SiteKey: testTurnstileWidgetSiteKey}) + if assert.NoError(t, err) { + assert.Equal(t, expectedTurnstileWidget, out, "rotate challenge_widgets structs not equal") + } +} + +func TestTurnstileWidgets_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey, 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, ` +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "sitekey": "0x4AAF00AAAABn0R22HWm-YUc", + "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc", + "created_on": "2014-01-01T05:20:00.123123Z", + "modified_on": "2014-01-01T05:20:00.123123Z", + "name": "blog.cloudflare.com login form", + "domains": [ + "203.0.113.1", + "cloudflare.com", + "blog.example.com" + ], + "mode": "invisible", + "bot_fight_mode": true, + "region": "world", + "offlabel": false + } +}`) + }) + + // Make sure missing account ID is thrown + err := client.DeleteTurnstileWidget(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + err = client.DeleteTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingSiteKey, err) + } + + err = client.DeleteTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), testTurnstileWidgetSiteKey) + assert.NoError(t, err) +} diff --git a/utils.go b/utils.go index 38d4315d76..bc2dc56338 100644 --- a/utils.go +++ b/utils.go @@ -16,7 +16,7 @@ func buildURI(path string, options interface{}) string { } // loadFixture takes a series of path components and returns the JSON fixture at -// that locationassociated. +// that location associated. func loadFixture(parts ...string) string { paths := []string{"testdata", "fixtures"} paths = append(paths, parts...) diff --git a/virtualdns.go b/virtualdns.go index ad4ed02575..d4bc91bd96 100644 --- a/virtualdns.go +++ b/virtualdns.go @@ -130,7 +130,7 @@ func (v VirtualDNS) vdnsUpgrade() DNSFirewallCluster { return DNSFirewallCluster{ ID: v.ID, Name: v.Name, - OriginIPs: v.OriginIPs, + UpstreamIPs: v.OriginIPs, DNSFirewallIPs: v.VirtualDNSIPs, MinimumCacheTTL: v.MinimumCacheTTL, MaximumCacheTTL: v.MaximumCacheTTL, @@ -147,7 +147,7 @@ func (v *DNSFirewallCluster) vdnsDowngrade() *VirtualDNS { return &VirtualDNS{ ID: v.ID, Name: v.Name, - OriginIPs: v.OriginIPs, + OriginIPs: v.UpstreamIPs, VirtualDNSIPs: v.DNSFirewallIPs, MinimumCacheTTL: v.MinimumCacheTTL, MaximumCacheTTL: v.MaximumCacheTTL, diff --git a/waiting_room.go b/waiting_room.go index a60b5028e3..737e49dc49 100644 --- a/waiting_room.go +++ b/waiting_room.go @@ -76,6 +76,12 @@ type WaitingRoomRule struct { Enabled *bool `json:"enabled"` } +// WaitingRoomSettings describes zone-level waiting room settings. +type WaitingRoomSettings struct { + // Whether to allow verified search engine crawlers to bypass all waiting rooms on this zone + SearchEngineCrawlerBypass bool `json:"search_engine_crawler_bypass"` +} + // WaitingRoomPagePreviewURL describes a WaitingRoomPagePreviewURL object. type WaitingRoomPagePreviewURL struct { PreviewURL string `json:"preview_url"` @@ -98,6 +104,12 @@ type WaitingRoomsResponse struct { Result []WaitingRoom `json:"result"` } +// WaitingRoomSettingsResponse is the API response, containing zone-level Waiting Room settings. +type WaitingRoomSettingsResponse struct { + Response + Result WaitingRoomSettings `json:"result"` +} + // WaitingRoomStatusResponse is the API response, containing the status of a waiting room. type WaitingRoomStatusResponse struct { Response @@ -532,3 +544,76 @@ func (api *API) DeleteWaitingRoomRule(ctx context.Context, rc *ResourceContainer return r.Result, nil } + +// GetWaitingRoomSettings fetches the Waiting Room zone-level settings for a zone. +// +// API reference: https://api.cloudflare.com/#waiting-room-get-zone-settings +func (api *API) GetWaitingRoomSettings(ctx context.Context, rc *ResourceContainer) (WaitingRoomSettings, error) { + if rc.Level != ZoneRouteLevel { + return WaitingRoomSettings{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + uri := fmt.Sprintf("/zones/%s/waiting_rooms/settings", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return WaitingRoomSettings{}, err + } + var r WaitingRoomSettingsResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WaitingRoomSettings{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +type PatchWaitingRoomSettingsParams struct { + SearchEngineCrawlerBypass *bool `json:"search_engine_crawler_bypass,omitempty"` +} + +// PatchWaitingRoomSettings lets you change individual zone-level Waiting Room settings. This is +// in contrast to UpdateWaitingRoomSettings which replaces all settings. +// +// API reference: https://api.cloudflare.com/#waiting-room-patch-zone-settings +func (api *API) PatchWaitingRoomSettings(ctx context.Context, rc *ResourceContainer, params PatchWaitingRoomSettingsParams) (WaitingRoomSettings, error) { + if rc.Level != ZoneRouteLevel { + return WaitingRoomSettings{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + uri := fmt.Sprintf("/zones/%s/waiting_rooms/settings", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params) + if err != nil { + return WaitingRoomSettings{}, err + } + var r WaitingRoomSettingsResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WaitingRoomSettings{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +type UpdateWaitingRoomSettingsParams struct { + SearchEngineCrawlerBypass *bool `json:"search_engine_crawler_bypass,omitempty"` +} + +// UpdateWaitingRoomSettings lets you replace all zone-level Waiting Room settings. This is in contrast to +// PatchWaitingRoomSettings which lets you change individual settings. +// +// API reference: https://api.cloudflare.com/#waiting-room-update-zone-settings +func (api *API) UpdateWaitingRoomSettings(ctx context.Context, rc *ResourceContainer, params UpdateWaitingRoomSettingsParams) (WaitingRoomSettings, error) { + if rc.Level != ZoneRouteLevel { + return WaitingRoomSettings{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + } + + uri := fmt.Sprintf("/zones/%s/waiting_rooms/settings", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return WaitingRoomSettings{}, err + } + var r WaitingRoomSettingsResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WaitingRoomSettings{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} diff --git a/waiting_room_test.go b/waiting_room_test.go index 902510305d..057789906b 100644 --- a/waiting_room_test.go +++ b/waiting_room_test.go @@ -97,6 +97,12 @@ var waitingRoomPagePreviewJSON = ` } ` +var waitingRoomSettingsJSON = ` + { + "search_engine_crawler_bypass": true + } + ` + var waitingRoom = WaitingRoom{ ID: waitingRoomID, CreatedOn: testTimestampWaitingRoom, @@ -160,6 +166,18 @@ var waitingRoomRule = WaitingRoomRule{ LastUpdated: &testTimestampWaitingRoom, } +var waitingRoomSettings = WaitingRoomSettings{ + SearchEngineCrawlerBypass: true, +} + +var waitingRoomSettingsUpdate = UpdateWaitingRoomSettingsParams{ + SearchEngineCrawlerBypass: BoolPtr(true), +} + +var waitingRoomSettingsPatch = PatchWaitingRoomSettingsParams{ + SearchEngineCrawlerBypass: BoolPtr(true), +} + func TestListWaitingRooms(t *testing.T) { setup() defer teardown() @@ -785,3 +803,78 @@ func TestReplaceWaitingRoomRules(t *testing.T) { assert.Equal(t, want, actual) } } + +func TestWaitingRoomSettings(t *testing.T) { + setup() + defer teardown() + + handler := 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.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": %s + } + `, waitingRoomSettingsJSON) + } + + mux.HandleFunc("/zones/"+testZoneID+"/waiting_rooms/settings", handler) + want := waitingRoomSettings + + actual, err := client.GetWaitingRoomSettings(context.Background(), ZoneIdentifier(testZoneID)) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateWaitingRoomSettings(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) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": %s + } + `, waitingRoomSettingsJSON) + } + + mux.HandleFunc("/zones/"+testZoneID+"/waiting_rooms/settings", handler) + want := waitingRoomSettings + + actual, err := client.UpdateWaitingRoomSettings(context.Background(), ZoneIdentifier(testZoneID), waitingRoomSettingsUpdate) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestChangeWaitingRoomSettings(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": %s + } + `, waitingRoomSettingsJSON) + } + + mux.HandleFunc("/zones/"+testZoneID+"/waiting_rooms/settings", handler) + want := waitingRoomSettings + + actual, err := client.PatchWaitingRoomSettings(context.Background(), ZoneIdentifier(testZoneID), waitingRoomSettingsPatch) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} diff --git a/workers.go b/workers.go index 7bc6e9a42d..b934218e69 100644 --- a/workers.go +++ b/workers.go @@ -3,10 +3,7 @@ package cloudflare import ( "bytes" "context" - rand "crypto/rand" - "encoding/hex" "encoding/json" - "errors" "fmt" "io" "mime" @@ -23,9 +20,38 @@ type WorkerRequestParams struct { ScriptName string } +type CreateWorkerParams struct { + ScriptName string + Script string + + // Module changes the Content-Type header to specify the script is an + // ES Module syntax script. + Module bool + + // Logpush opts the worker into Workers Logpush logging. A nil value leaves the current setting unchanged. + // https://developers.cloudflare.com/workers/platform/logpush/ + Logpush *bool + + // Bindings should be a map where the keys are the binding name, and the + // values are the binding content + Bindings map[string]WorkerBinding + + // CompatibilityDate is a date in the form yyyy-mm-dd, + // which will be used to determine which version of the Workers runtime is used. + // https://developers.cloudflare.com/workers/platform/compatibility-dates/ + CompatibilityDate string + + // CompatibilityFlags are the names of features of the Workers runtime to be enabled or disabled, + // usually used together with CompatibilityDate. + // https://developers.cloudflare.com/workers/platform/compatibility-dates/#compatibility-flags + CompatibilityFlags []string + + Placement *Placement +} + // WorkerScriptParams provides a worker script and the associated bindings. type WorkerScriptParams struct { - Script string + ScriptName string // Module changes the Content-Type header to specify the script is an // ES Module syntax script. @@ -40,10 +66,9 @@ type WorkerScriptParams struct { // // API reference: https://api.cloudflare.com/#worker-routes-properties type WorkerRoute struct { - ID string `json:"id,omitempty"` - Pattern string `json:"pattern"` - Enabled bool `json:"enabled"` // this is deprecated: https://api.cloudflare.com/#worker-filters-deprecated--properties - Script string `json:"script,omitempty"` + ID string `json:"id,omitempty"` + Pattern string `json:"pattern"` + ScriptName string `json:"script,omitempty"` } // WorkerRoutesResponse embeds Response struct and slice of WorkerRoutes. @@ -67,16 +92,21 @@ type WorkerScript struct { // WorkerMetaData contains worker script information such as size, creation & modification dates. type WorkerMetaData struct { - ID string `json:"id,omitempty"` - ETAG string `json:"etag,omitempty"` - Size int `json:"size,omitempty"` - CreatedOn time.Time `json:"created_on,omitempty"` - ModifiedOn time.Time `json:"modified_on,omitempty"` + ID string `json:"id,omitempty"` + ETAG string `json:"etag,omitempty"` + Size int `json:"size,omitempty"` + CreatedOn time.Time `json:"created_on,omitempty"` + ModifiedOn time.Time `json:"modified_on,omitempty"` + Logpush *bool `json:"logpush,omitempty"` + LastDeployedFrom *string `json:"last_deployed_from,omitempty"` + DeploymentId *string `json:"deployment_id,omitempty"` + PlacementMode *PlacementMode `json:"placement_mode,omitempty"` } // WorkerListResponse wrapper struct for API response to worker script list API call. type WorkerListResponse struct { Response + ResultInfo WorkerList []WorkerMetaData `json:"result"` } @@ -87,375 +117,65 @@ type WorkerScriptResponse struct { WorkerScript `json:"result"` } -// WorkerBindingType represents a particular type of binding. -type WorkerBindingType string - -func (b WorkerBindingType) String() string { - return string(b) -} - -const ( - // WorkerDurableObjectBindingType is the type for Durable Object bindings. - WorkerDurableObjectBindingType WorkerBindingType = "durable_object_namespace" - // WorkerInheritBindingType is the type for inherited bindings. - WorkerInheritBindingType WorkerBindingType = "inherit" - // WorkerKvNamespaceBindingType is the type for KV Namespace bindings. - WorkerKvNamespaceBindingType WorkerBindingType = "kv_namespace" - // WorkerWebAssemblyBindingType is the type for Web Assembly module bindings. - WorkerWebAssemblyBindingType WorkerBindingType = "wasm_module" - // WorkerSecretTextBindingType is the type for secret text bindings. - WorkerSecretTextBindingType WorkerBindingType = "secret_text" - // WorkerPlainTextBindingType is the type for plain text bindings. - WorkerPlainTextBindingType WorkerBindingType = "plain_text" - // WorkerServiceBindingType is the type for service bindings. - WorkerServiceBindingType WorkerBindingType = "service" - // WorkerR2BucketBindingType is the type for R2 bucket bindings. - WorkerR2BucketBindingType WorkerBindingType = "r2_bucket" - // WorkerAnalyticsEngineBindingType is the type for Analytics Engine dataset bindings. - WorkerAnalyticsEngineBindingType WorkerBindingType = "analytics_engine" -) - -// WorkerBindingListItem a struct representing an individual binding in a list of bindings. -type WorkerBindingListItem struct { - Name string `json:"name"` - Binding WorkerBinding -} - -// WorkerBindingListResponse wrapper struct for API response to worker binding list API call. -type WorkerBindingListResponse struct { - Response - BindingList []WorkerBindingListItem -} - -// Workers supports multiple types of bindings, e.g. KV namespaces or WebAssembly modules, and each type -// of binding will be represented differently in the upload request body. At a high-level, every binding -// will specify metadata, which is a JSON object with the properties "name" and "type". Some types of bindings -// will also have additional metadata properties. For example, KV bindings also specify the KV namespace. -// In addition to the metadata, some binding types may need to include additional data as part of the -// multipart form. For example, WebAssembly bindings will include the contents of the WebAssembly module. - -// WorkerBinding is the generic interface implemented by all of -// the various binding types. -type WorkerBinding interface { - Type() WorkerBindingType - - // serialize is responsible for returning the binding metadata as well as an optionally - // returning a function that can modify the multipart form body. For example, this is used - // by WebAssembly bindings to add a new part containing the WebAssembly module contents. - serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) -} - -// workerBindingMeta is the metadata portion of the binding. -type workerBindingMeta = map[string]interface{} - -// workerBindingBodyWriter allows for a binding to add additional parts to the multipart body. -type workerBindingBodyWriter func(*multipart.Writer) error - -// WorkerInheritBinding will just persist whatever binding content was previously uploaded. -type WorkerInheritBinding struct { - // Optional parameter that allows for renaming a binding without changing - // its contents. If `OldName` is empty, the binding name will not be changed. - OldName string -} - -// Type returns the type of the binding. -func (b WorkerInheritBinding) Type() WorkerBindingType { - return WorkerInheritBindingType -} - -func (b WorkerInheritBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - meta := workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - } - - if b.OldName != "" { - meta["old_name"] = b.OldName - } - - return meta, nil, nil -} - -// WorkerKvNamespaceBinding is a binding to a Workers KV Namespace -// -// https://developers.cloudflare.com/workers/archive/api/resource-bindings/kv-namespaces/ -type WorkerKvNamespaceBinding struct { - NamespaceID string -} - -// Type returns the type of the binding. -func (b WorkerKvNamespaceBinding) Type() WorkerBindingType { - return WorkerKvNamespaceBindingType -} +type ListWorkersParams struct{} -func (b WorkerKvNamespaceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.NamespaceID == "" { - return nil, nil, fmt.Errorf(`NamespaceID for binding "%s" cannot be empty`, bindingName) - } - - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "namespace_id": b.NamespaceID, - }, nil, nil -} - -// WorkerDurableObjectBinding is a binding to a Workers Durable Object -// -// https://api.cloudflare.com/#durable-objects-namespace-properties -type WorkerDurableObjectBinding struct { - ClassName string +type DeleteWorkerParams struct { ScriptName string } -// Type returns the type of the binding. -func (b WorkerDurableObjectBinding) Type() WorkerBindingType { - return WorkerDurableObjectBindingType -} - -func (b WorkerDurableObjectBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.ClassName == "" { - return nil, nil, fmt.Errorf(`ClassName for binding "%s" cannot be empty`, bindingName) - } - - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "class_name": b.ClassName, - "script_name": b.ScriptName, - }, nil, nil -} - -// WorkerWebAssemblyBinding is a binding to a WebAssembly module -// -// https://developers.cloudflare.com/workers/archive/api/resource-bindings/webassembly-modules/ -type WorkerWebAssemblyBinding struct { - Module io.Reader -} - -// Type returns the type of the binding. -func (b WorkerWebAssemblyBinding) Type() WorkerBindingType { - return WorkerWebAssemblyBindingType -} - -func (b WorkerWebAssemblyBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - partName := getRandomPartName() - - bodyWriter := func(mpw *multipart.Writer) error { - var hdr = textproto.MIMEHeader{} - hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, partName)) - hdr.Set("content-type", "application/wasm") - pw, err := mpw.CreatePart(hdr) - if err != nil { - return err - } - _, err = io.Copy(pw, b.Module) - return err - } +type PlacementMode string - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "part": partName, - }, bodyWriter, nil -} - -// WorkerPlainTextBinding is a binding to plain text -// -// https://developers.cloudflare.com/workers/tooling/api/scripts/#add-a-plain-text-binding -type WorkerPlainTextBinding struct { - Text string -} - -// Type returns the type of the binding. -func (b WorkerPlainTextBinding) Type() WorkerBindingType { - return WorkerPlainTextBindingType -} - -func (b WorkerPlainTextBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.Text == "" { - return nil, nil, fmt.Errorf(`Text for binding "%s" cannot be empty`, bindingName) - } +const ( + PlacementModeOff PlacementMode = "" + PlacementModeSmart PlacementMode = "smart" +) - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "text": b.Text, - }, nil, nil +type Placement struct { + Mode PlacementMode `json:"mode"` } -// WorkerSecretTextBinding is a binding to secret text +// DeleteWorker deletes a single Worker. // -// https://developers.cloudflare.com/workers/tooling/api/scripts/#add-a-secret-text-binding -type WorkerSecretTextBinding struct { - Text string -} - -// Type returns the type of the binding. -func (b WorkerSecretTextBinding) Type() WorkerBindingType { - return WorkerSecretTextBindingType -} - -func (b WorkerSecretTextBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.Text == "" { - return nil, nil, fmt.Errorf(`Text for binding "%s" cannot be empty`, bindingName) - } - - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "text": b.Text, - }, nil, nil -} - -type WorkerServiceBinding struct { - Service string - Environment *string -} - -func (b WorkerServiceBinding) Type() WorkerBindingType { - return WorkerServiceBindingType -} - -func (b WorkerServiceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.Service == "" { - return nil, nil, fmt.Errorf(`Service for binding "%s" cannot be empty`, bindingName) - } - - meta := workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "service": b.Service, - } - - if b.Environment != nil { - meta["environment"] = *b.Environment - } - - return meta, nil, nil -} - -// WorkerR2BucketBinding is a binding to an R2 bucket. -type WorkerR2BucketBinding struct { - BucketName string -} - -// Type returns the type of the binding. -func (b WorkerR2BucketBinding) Type() WorkerBindingType { - return WorkerR2BucketBindingType -} - -func (b WorkerR2BucketBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.BucketName == "" { - return nil, nil, fmt.Errorf(`BucketName for binding "%s" cannot be empty`, bindingName) +// API reference: https://api.cloudflare.com/#worker-script-delete-worker +func (api *API) DeleteWorker(ctx context.Context, rc *ResourceContainer, params DeleteWorkerParams) error { + if rc.Level != AccountRouteLevel { + return ErrRequiredAccountLevelResourceContainer } - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "bucket_name": b.BucketName, - }, nil, nil -} - -// WorkerAnalyticsEngineBinding is a binding to an Analytics Engine dataset. -type WorkerAnalyticsEngineBinding struct { - Dataset string -} - -// Type returns the type of the binding. -func (b WorkerAnalyticsEngineBinding) Type() WorkerBindingType { - return WorkerAnalyticsEngineBindingType -} - -func (b WorkerAnalyticsEngineBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { - if b.Dataset == "" { - return nil, nil, fmt.Errorf(`Dataset for binding "%s" cannot be empty`, bindingName) + if rc.Identifier == "" { + return ErrMissingAccountID } - return workerBindingMeta{ - "name": bindingName, - "type": b.Type(), - "dataset": b.Dataset, - }, 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 { - randBytes := make([]byte, 16) - rand.Read(randBytes) //nolint:errcheck - return hex.EncodeToString(randBytes) -} - -// DeleteWorker deletes worker for a zone. -// -// API reference: https://api.cloudflare.com/#worker-script-delete-worker -func (api *API) DeleteWorker(ctx context.Context, requestParams *WorkerRequestParams) (WorkerScriptResponse, error) { - // if ScriptName is provided we will treat as org request - if requestParams.ScriptName != "" { - return api.deleteWorkerWithName(ctx, requestParams.ScriptName) - } - uri := fmt.Sprintf("/zones/%s/workers/script", requestParams.ZoneID) + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, params.ScriptName) res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + var r WorkerScriptResponse if err != nil { - return r, err + return err } + err = json.Unmarshal(res, &r) if err != nil { - return r, fmt.Errorf("%s: %w", errUnmarshalError, err) + return fmt.Errorf("%s: %w", errUnmarshalError, err) } - return r, nil + + return nil } -// DeleteWorkerWithName deletes worker for a zone. -// Sccount must be specified as api option https://godoc.org/github.com/cloudflare/cloudflare-go#UsingAccount +// GetWorker fetch raw script content for your worker returns string containing +// worker code js. // // API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/ -func (api *API) deleteWorkerWithName(ctx context.Context, scriptName string) (WorkerScriptResponse, error) { - if api.AccountID == "" { - return WorkerScriptResponse{}, errors.New("account ID required") +func (api *API) GetWorker(ctx context.Context, rc *ResourceContainer, scriptName string) (WorkerScriptResponse, error) { + if rc.Level != AccountRouteLevel { + return WorkerScriptResponse{}, ErrRequiredAccountLevelResourceContainer } - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", api.AccountID, scriptName) - res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) - var r WorkerScriptResponse - if err != nil { - return r, err - } - err = json.Unmarshal(res, &r) - if err != nil { - return r, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - return r, nil -} -// DownloadWorker fetch raw script content for your worker returns []byte containing worker code js -// -// API reference: https://api.cloudflare.com/#worker-script-download-worker -func (api *API) DownloadWorker(ctx context.Context, requestParams *WorkerRequestParams) (WorkerScriptResponse, error) { - if requestParams.ScriptName != "" { - return api.downloadWorkerWithName(ctx, requestParams.ScriptName) + if rc.Identifier == "" { + return WorkerScriptResponse{}, ErrMissingAccountID } - uri := fmt.Sprintf("/zones/%s/workers/script", requestParams.ZoneID) - res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) - var r WorkerScriptResponse - if err != nil { - return r, err - } - r.Script = string(res) - r.Module = false - r.Success = true - return r, nil -} -// DownloadWorkerWithName fetch raw script content for your worker returns string containing worker code js -// -// API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/ -func (api *API) downloadWorkerWithName(ctx context.Context, scriptName string) (WorkerScriptResponse, error) { - if api.AccountID == "" { - return WorkerScriptResponse{}, errors.New("account ID required") - } - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", api.AccountID, scriptName) + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, scriptName) res, err := api.makeRequestContextWithHeadersComplete(ctx, http.MethodGet, uri, nil, nil) var r WorkerScriptResponse if err != nil { @@ -486,233 +206,78 @@ func (api *API) downloadWorkerWithName(ctx context.Context, scriptName string) ( return r, nil } -// ListWorkerBindings returns all the bindings for a particular worker. -func (api *API) ListWorkerBindings(ctx context.Context, requestParams *WorkerRequestParams) (WorkerBindingListResponse, error) { - if requestParams.ScriptName == "" { - return WorkerBindingListResponse{}, errors.New("ScriptName is required") - } - if api.AccountID == "" { - return WorkerBindingListResponse{}, errors.New("account ID required") - } - - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings", api.AccountID, requestParams.ScriptName) - - var jsonRes struct { - Response - Bindings []workerBindingMeta `json:"result"` - } - var r WorkerBindingListResponse - res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) - if err != nil { - return r, err - } - err = json.Unmarshal(res, &jsonRes) - if err != nil { - return r, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - - r = WorkerBindingListResponse{ - Response: jsonRes.Response, - BindingList: make([]WorkerBindingListItem, 0, len(jsonRes.Bindings)), - } - for _, jsonBinding := range jsonRes.Bindings { - name, ok := jsonBinding["name"].(string) - if !ok { - return r, fmt.Errorf("Binding missing name %v", jsonBinding) - } - bType, ok := jsonBinding["type"].(string) - if !ok { - return r, fmt.Errorf("Binding missing type %v", jsonBinding) - } - bindingListItem := WorkerBindingListItem{ - Name: name, - } - - switch WorkerBindingType(bType) { - case WorkerDurableObjectBindingType: - class_name := jsonBinding["class_name"].(string) - script_name := jsonBinding["script_name"].(string) - bindingListItem.Binding = WorkerDurableObjectBinding{ - ClassName: class_name, - ScriptName: script_name, - } - case WorkerKvNamespaceBindingType: - namespaceID := jsonBinding["namespace_id"].(string) - bindingListItem.Binding = WorkerKvNamespaceBinding{ - NamespaceID: namespaceID, - } - case WorkerWebAssemblyBindingType: - bindingListItem.Binding = WorkerWebAssemblyBinding{ - Module: &bindingContentReader{ - ctx: ctx, - api: api, - requestParams: requestParams, - bindingName: name, - }, - } - case WorkerPlainTextBindingType: - text := jsonBinding["text"].(string) - bindingListItem.Binding = WorkerPlainTextBinding{ - Text: text, - } - case WorkerServiceBindingType: - service := jsonBinding["service"].(string) - environment := jsonBinding["environment"].(string) - bindingListItem.Binding = WorkerServiceBinding{ - Service: service, - Environment: &environment, - } - case WorkerSecretTextBindingType: - bindingListItem.Binding = WorkerSecretTextBinding{} - case WorkerR2BucketBindingType: - bucketName := jsonBinding["bucket_name"].(string) - bindingListItem.Binding = WorkerR2BucketBinding{ - BucketName: bucketName, - } - case WorkerAnalyticsEngineBindingType: - dataset := jsonBinding["dataset"].(string) - bindingListItem.Binding = WorkerAnalyticsEngineBinding{ - Dataset: dataset, - } - default: - bindingListItem.Binding = WorkerInheritBinding{} - } - r.BindingList = append(r.BindingList, bindingListItem) - } - - return r, nil -} - -// bindingContentReader is an io.Reader that will lazily load the -// raw bytes for a binding from the API when the Read() method -// is first called. This is only useful for binding types -// that store raw bytes, like WebAssembly modules. -type bindingContentReader struct { - api *API - requestParams *WorkerRequestParams - ctx context.Context - bindingName string - content []byte - position int -} - -func (b *bindingContentReader) Read(p []byte) (n int, err error) { - // Lazily load the content when Read() is first called - if b.content == nil { - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings/%s/content", b.api.AccountID, b.requestParams.ScriptName, b.bindingName) - res, err := b.api.makeRequestContext(b.ctx, http.MethodGet, uri, nil) - if err != nil { - return 0, err - } - b.content = res - } - - if b.position >= len(b.content) { - return 0, io.EOF - } - - bytesRemaining := len(b.content) - b.position - bytesToProcess := 0 - if len(p) < bytesRemaining { - bytesToProcess = len(p) - } else { - bytesToProcess = bytesRemaining +// ListWorkers returns list of Workers for given account. +// +// API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/ +func (api *API) ListWorkers(ctx context.Context, rc *ResourceContainer, params ListWorkersParams) (WorkerListResponse, *ResultInfo, error) { + if rc.Level != AccountRouteLevel { + return WorkerListResponse{}, &ResultInfo{}, ErrRequiredAccountLevelResourceContainer } - for i := 0; i < bytesToProcess; i++ { - p[i] = b.content[b.position] - b.position = b.position + 1 + if rc.Identifier == "" { + return WorkerListResponse{}, &ResultInfo{}, ErrMissingAccountID } - return bytesToProcess, nil -} - -// ListWorkerScripts returns list of worker scripts for given account. -// -// API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/ -func (api *API) ListWorkerScripts(ctx context.Context) (WorkerListResponse, error) { - if api.AccountID == "" { - return WorkerListResponse{}, errors.New("account ID required") - } - uri := fmt.Sprintf("/accounts/%s/workers/scripts", api.AccountID) + uri := fmt.Sprintf("/accounts/%s/workers/scripts", rc.Identifier) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { - return WorkerListResponse{}, err + return WorkerListResponse{}, &ResultInfo{}, err } + var r WorkerListResponse err = json.Unmarshal(res, &r) if err != nil { - return WorkerListResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + return WorkerListResponse{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err) } - return r, nil + + return r, &r.ResultInfo, nil } -// UploadWorker push raw script content for your worker. +// UploadWorker pushes raw script content for your Worker. // // API reference: https://api.cloudflare.com/#worker-script-upload-worker -func (api *API) UploadWorker(ctx context.Context, requestParams *WorkerRequestParams, params *WorkerScriptParams) (WorkerScriptResponse, error) { - if params.Module { - return api.UploadWorkerWithBindings(ctx, requestParams, params) +func (api *API) UploadWorker(ctx context.Context, rc *ResourceContainer, params CreateWorkerParams) (WorkerScriptResponse, error) { + if rc.Level != AccountRouteLevel { + return WorkerScriptResponse{}, ErrRequiredAccountLevelResourceContainer } - contentType := "application/javascript" - if requestParams.ScriptName != "" { - return api.uploadWorkerWithName(ctx, requestParams.ScriptName, contentType, []byte(params.Script)) + if rc.Identifier == "" { + return WorkerScriptResponse{}, ErrMissingAccountID } - return api.uploadWorkerForZone(ctx, requestParams.ZoneID, contentType, []byte(params.Script)) -} -// UploadWorkerWithBindings push raw script content and bindings for your worker -// -// API reference: https://api.cloudflare.com/#worker-script-upload-worker -func (api *API) UploadWorkerWithBindings(ctx context.Context, requestParams *WorkerRequestParams, data *WorkerScriptParams) (WorkerScriptResponse, error) { - contentType, body, err := formatMultipartBody(data) - if err != nil { - return WorkerScriptResponse{}, err - } - if requestParams.ScriptName != "" { - return api.uploadWorkerWithName(ctx, requestParams.ScriptName, contentType, body) - } - return api.uploadWorkerForZone(ctx, requestParams.ZoneID, contentType, body) -} + body := []byte(params.Script) + var ( + contentType = "application/javascript" + err error + ) -func (api *API) uploadWorkerForZone(ctx context.Context, zoneID, contentType string, body []byte) (WorkerScriptResponse, error) { - uri := fmt.Sprintf("/zones/%s/workers/script", zoneID) - headers := make(http.Header) - headers.Set("Content-Type", contentType) - res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers) - var r WorkerScriptResponse - if err != nil { - return r, err - } - err = json.Unmarshal(res, &r) - if err != nil { - return r, fmt.Errorf("%s: %w", errUnmarshalError, err) + if params.Module || params.Logpush != nil || params.Placement != nil || len(params.Bindings) > 0 || params.CompatibilityDate != "" || len(params.CompatibilityFlags) > 0 { + contentType, body, err = formatMultipartBody(params) + if err != nil { + return WorkerScriptResponse{}, err + } } - return r, nil -} -func (api *API) uploadWorkerWithName(ctx context.Context, scriptName, contentType string, body []byte) (WorkerScriptResponse, error) { - if api.AccountID == "" { - return WorkerScriptResponse{}, errors.New("account ID required") - } - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", api.AccountID, scriptName) + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, params.ScriptName) headers := make(http.Header) headers.Set("Content-Type", contentType) res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers) + var r WorkerScriptResponse if err != nil { return r, err } + err = json.Unmarshal(res, &r) if err != nil { return r, fmt.Errorf("%s: %w", errUnmarshalError, err) } + return r, nil } // Returns content-type, body, error. -func formatMultipartBody(params *WorkerScriptParams) (string, []byte, error) { +func formatMultipartBody(params CreateWorkerParams) (string, []byte, error) { var buf = &bytes.Buffer{} var mpw = multipart.NewWriter(buf) defer mpw.Close() @@ -720,11 +285,19 @@ func formatMultipartBody(params *WorkerScriptParams) (string, []byte, error) { // Write metadata part var scriptPartName string meta := struct { - BodyPart string `json:"body_part,omitempty"` - MainModule string `json:"main_module,omitempty"` - Bindings []workerBindingMeta `json:"bindings"` + BodyPart string `json:"body_part,omitempty"` + MainModule string `json:"main_module,omitempty"` + Bindings []workerBindingMeta `json:"bindings"` + Logpush *bool `json:"logpush,omitempty"` + CompatibilityDate string `json:"compatibility_date,omitempty"` + CompatibilityFlags []string `json:"compatibility_flags,omitempty"` + Placement *Placement `json:"placement,omitempty"` }{ - Bindings: make([]workerBindingMeta, 0, len(params.Bindings)), + Bindings: make([]workerBindingMeta, 0, len(params.Bindings)), + Logpush: params.Logpush, + CompatibilityDate: params.CompatibilityDate, + CompatibilityFlags: params.CompatibilityFlags, + Placement: params.Placement, } if params.Module { @@ -797,174 +370,3 @@ func formatMultipartBody(params *WorkerScriptParams) (string, []byte, error) { return mpw.FormDataContentType(), buf.Bytes(), nil } - -// CreateWorkerRoute creates worker route for a zone -// -// API reference: https://api.cloudflare.com/#worker-filters-create-filter, https://api.cloudflare.com/#worker-routes-create-route -func (api *API) CreateWorkerRoute(ctx context.Context, zoneID string, route WorkerRoute) (WorkerRouteResponse, error) { - pathComponent, err := getRouteEndpoint(route) - if err != nil { - return WorkerRouteResponse{}, err - } - - uri := fmt.Sprintf("/zones/%s/workers/%s", zoneID, pathComponent) - res, err := api.makeRequestContext(ctx, http.MethodPost, uri, route) - if err != nil { - return WorkerRouteResponse{}, err - } - var r WorkerRouteResponse - err = json.Unmarshal(res, &r) - if err != nil { - return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - return r, nil -} - -// DeleteWorkerRoute deletes worker route for a zone -// -// API reference: https://api.cloudflare.com/#worker-routes-delete-route -func (api *API) DeleteWorkerRoute(ctx context.Context, zoneID string, routeID string) (WorkerRouteResponse, error) { - uri := fmt.Sprintf("/zones/%s/workers/routes/%s", zoneID, routeID) - res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) - if err != nil { - return WorkerRouteResponse{}, err - } - var r WorkerRouteResponse - err = json.Unmarshal(res, &r) - if err != nil { - return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - return r, nil -} - -// ListWorkerRoutes returns list of worker routes -// -// API reference: https://api.cloudflare.com/#worker-filters-list-filters, https://api.cloudflare.com/#worker-routes-list-routes -func (api *API) ListWorkerRoutes(ctx context.Context, zoneID string) (WorkerRoutesResponse, error) { - pathComponent := "filters" - // Unfortunately we don't have a good signal of whether the user is wanting - // to use the deprecated filters endpoint (https://api.cloudflare.com/#worker-filters-list-filters) - // or the multi-script routes endpoint (https://api.cloudflare.com/#worker-script-list-workers) - // - // The filters endpoint does not support API tokens, so if an API token is specified we need to use - // the routes endpoint. Otherwise, since the multi-script API endpoints that operate on a script - // require an AccountID, we assume that anyone specifying an AccountID is using the routes endpoint. - // This is likely too presumptuous. In the next major version, we should just remove the deprecated - // filter endpoints entirely to avoid this ambiguity. - if api.AccountID != "" || api.APIToken != "" { - pathComponent = "routes" - } - uri := fmt.Sprintf("/zones/%s/workers/%s", zoneID, pathComponent) - res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) - if err != nil { - return WorkerRoutesResponse{}, err - } - var r WorkerRoutesResponse - err = json.Unmarshal(res, &r) - if err != nil { - return WorkerRoutesResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - for i := range r.Routes { - route := &r.Routes[i] - // The Enabled flag will not be set in the multi-script API response - // so we manually set it to true if the script name is not empty - // in case any multi-script customers rely on the Enabled field - if route.Script != "" { - route.Enabled = true - } - } - return r, nil -} - -// GetWorkerRoute returns a worker route. -// -// API reference: https://api.cloudflare.com/#worker-routes-get-route -func (api *API) GetWorkerRoute(ctx context.Context, zoneID string, routeID string) (WorkerRouteResponse, error) { - uri := fmt.Sprintf("/zones/%s/workers/routes/%s", zoneID, routeID) - res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) - if err != nil { - return WorkerRouteResponse{}, err - } - var r WorkerRouteResponse - err = json.Unmarshal(res, &r) - if err != nil { - return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - return r, nil -} - -// UpdateWorkerRoute updates worker route for a zone. -// -// API reference: https://api.cloudflare.com/#worker-filters-update-filter, https://api.cloudflare.com/#worker-routes-update-route -func (api *API) UpdateWorkerRoute(ctx context.Context, zoneID string, routeID string, route WorkerRoute) (WorkerRouteResponse, error) { - pathComponent, err := getRouteEndpoint(route) - if err != nil { - return WorkerRouteResponse{}, err - } - uri := fmt.Sprintf("/zones/%s/workers/%s/%s", zoneID, pathComponent, routeID) - res, err := api.makeRequestContext(ctx, http.MethodPut, uri, route) - if err != nil { - return WorkerRouteResponse{}, err - } - var r WorkerRouteResponse - err = json.Unmarshal(res, &r) - if err != nil { - return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - return r, nil -} - -func getRouteEndpoint(route WorkerRoute) (string, error) { - if route.Script != "" && route.Enabled { - return "", errors.New("Only `Script` or `Enabled` may be specified for a WorkerRoute, not both") - } - - // For backwards-compatibility, fallback to the deprecated filter - // endpoint if Enabled == true - // https://api.cloudflare.com/#worker-filters-deprecated--properties - if route.Enabled { - return "filters", nil - } - - return "routes", nil -} - -type WorkerDomainParams struct { - ZoneID string `json:"zone_id"` - Hostname string `json:"hostname"` - Service string `json:"service"` - Environment string `json:"environment,omitempty"` -} - -type WorkerDomainResult struct { - ID string `json:"id"` - ZoneID string `json:"zone_id"` - ZoneName string `json:"zone_name"` - Hostname string `json:"hostname"` - Service string `json:"service"` - Environment string `json:"environment"` -} - -type WorkerDomainResponse struct { - Response - WorkerDomainResult `json:"result"` -} - -// AttachWorkerToDomain attaches a worker to a zone and hostname -// -// API reference: https://api.cloudflare.com/#worker-domain-attach-to-domain -func (api *API) AttachWorkerToDomain(ctx context.Context, rc *ResourceContainer, params *WorkerDomainParams) (WorkerDomainResponse, error) { - uri := fmt.Sprintf("/accounts/%s/workers/domains", rc.Identifier) - res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) - if err != nil { - return WorkerDomainResponse{}, err - } - - var r WorkerDomainResponse - err = json.Unmarshal(res, &r) - if err != nil { - return WorkerDomainResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) - } - - return r, nil -} diff --git a/workers_account_settings.go b/workers_account_settings.go index d8e58516a0..6b3b65d6df 100644 --- a/workers_account_settings.go +++ b/workers_account_settings.go @@ -37,6 +37,10 @@ func (api *API) CreateWorkersAccountSettings(ctx context.Context, rc *ResourceCo return WorkersAccountSettings{}, ErrMissingAccountID } + if rc.Level != AccountRouteLevel { + return WorkersAccountSettings{}, ErrRequiredAccountLevelResourceContainer + } + uri := fmt.Sprintf("/accounts/%s/workers/account-settings", rc.Identifier) res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) if err != nil { @@ -59,6 +63,10 @@ func (api *API) WorkersAccountSettings(ctx context.Context, rc *ResourceContaine return WorkersAccountSettings{}, ErrMissingAccountID } + if rc.Level != AccountRouteLevel { + return WorkersAccountSettings{}, ErrRequiredAccountLevelResourceContainer + } + uri := fmt.Sprintf("/accounts/%s/workers/account-settings", rc.Identifier) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, params) if err != nil { diff --git a/workers_bindings.go b/workers_bindings.go new file mode 100644 index 0000000000..11f4ba7cd0 --- /dev/null +++ b/workers_bindings.go @@ -0,0 +1,502 @@ +package cloudflare + +import ( + "context" + rand "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" +) + +// WorkerBindingType represents a particular type of binding. +type WorkerBindingType string + +func (b WorkerBindingType) String() string { + return string(b) +} + +const ( + // WorkerDurableObjectBindingType is the type for Durable Object bindings. + WorkerDurableObjectBindingType WorkerBindingType = "durable_object_namespace" + // WorkerInheritBindingType is the type for inherited bindings. + WorkerInheritBindingType WorkerBindingType = "inherit" + // WorkerKvNamespaceBindingType is the type for KV Namespace bindings. + WorkerKvNamespaceBindingType WorkerBindingType = "kv_namespace" + // WorkerWebAssemblyBindingType is the type for Web Assembly module bindings. + WorkerWebAssemblyBindingType WorkerBindingType = "wasm_module" + // WorkerSecretTextBindingType is the type for secret text bindings. + WorkerSecretTextBindingType WorkerBindingType = "secret_text" + // WorkerPlainTextBindingType is the type for plain text bindings. + WorkerPlainTextBindingType WorkerBindingType = "plain_text" + // WorkerServiceBindingType is the type for service bindings. + WorkerServiceBindingType WorkerBindingType = "service" + // WorkerR2BucketBindingType is the type for R2 bucket bindings. + WorkerR2BucketBindingType WorkerBindingType = "r2_bucket" + // WorkerAnalyticsEngineBindingType is the type for Analytics Engine dataset bindings. + WorkerAnalyticsEngineBindingType WorkerBindingType = "analytics_engine" + // WorkerQueueBindingType is the type for queue bindings. + WorkerQueueBindingType WorkerBindingType = "queue" +) + +type ListWorkerBindingsParams struct { + ScriptName string +} + +// WorkerBindingListItem a struct representing an individual binding in a list of bindings. +type WorkerBindingListItem struct { + Name string `json:"name"` + Binding WorkerBinding +} + +// WorkerBindingListResponse wrapper struct for API response to worker binding list API call. +type WorkerBindingListResponse struct { + Response + BindingList []WorkerBindingListItem +} + +// Workers supports multiple types of bindings, e.g. KV namespaces or WebAssembly modules, and each type +// of binding will be represented differently in the upload request body. At a high-level, every binding +// will specify metadata, which is a JSON object with the properties "name" and "type". Some types of bindings +// will also have additional metadata properties. For example, KV bindings also specify the KV namespace. +// In addition to the metadata, some binding types may need to include additional data as part of the +// multipart form. For example, WebAssembly bindings will include the contents of the WebAssembly module. + +// WorkerBinding is the generic interface implemented by all of +// the various binding types. +type WorkerBinding interface { + Type() WorkerBindingType + + // serialize is responsible for returning the binding metadata as well as an optionally + // returning a function that can modify the multipart form body. For example, this is used + // by WebAssembly bindings to add a new part containing the WebAssembly module contents. + serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) +} + +// workerBindingMeta is the metadata portion of the binding. +type workerBindingMeta = map[string]interface{} + +// workerBindingBodyWriter allows for a binding to add additional parts to the multipart body. +type workerBindingBodyWriter func(*multipart.Writer) error + +// WorkerInheritBinding will just persist whatever binding content was previously uploaded. +type WorkerInheritBinding struct { + // Optional parameter that allows for renaming a binding without changing + // its contents. If `OldName` is empty, the binding name will not be changed. + OldName string +} + +// Type returns the type of the binding. +func (b WorkerInheritBinding) Type() WorkerBindingType { + return WorkerInheritBindingType +} + +func (b WorkerInheritBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + meta := workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + } + + if b.OldName != "" { + meta["old_name"] = b.OldName + } + + return meta, nil, nil +} + +// WorkerKvNamespaceBinding is a binding to a Workers KV Namespace. +// +// https://developers.cloudflare.com/workers/archive/api/resource-bindings/kv-namespaces/ +type WorkerKvNamespaceBinding struct { + NamespaceID string +} + +// Type returns the type of the binding. +func (b WorkerKvNamespaceBinding) Type() WorkerBindingType { + return WorkerKvNamespaceBindingType +} + +func (b WorkerKvNamespaceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.NamespaceID == "" { + return nil, nil, fmt.Errorf(`NamespaceID for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "namespace_id": b.NamespaceID, + }, nil, nil +} + +// WorkerDurableObjectBinding is a binding to a Workers Durable Object. +// +// https://api.cloudflare.com/#durable-objects-namespace-properties +type WorkerDurableObjectBinding struct { + ClassName string + ScriptName string +} + +// Type returns the type of the binding. +func (b WorkerDurableObjectBinding) Type() WorkerBindingType { + return WorkerDurableObjectBindingType +} + +func (b WorkerDurableObjectBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.ClassName == "" { + return nil, nil, fmt.Errorf(`ClassName for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "class_name": b.ClassName, + "script_name": b.ScriptName, + }, nil, nil +} + +// WorkerWebAssemblyBinding is a binding to a WebAssembly module. +// +// https://developers.cloudflare.com/workers/archive/api/resource-bindings/webassembly-modules/ +type WorkerWebAssemblyBinding struct { + Module io.Reader +} + +// Type returns the type of the binding. +func (b WorkerWebAssemblyBinding) Type() WorkerBindingType { + return WorkerWebAssemblyBindingType +} + +func (b WorkerWebAssemblyBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + partName := getRandomPartName() + + bodyWriter := func(mpw *multipart.Writer) error { + var hdr = textproto.MIMEHeader{} + hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, partName)) + hdr.Set("content-type", "application/wasm") + pw, err := mpw.CreatePart(hdr) + if err != nil { + return err + } + _, err = io.Copy(pw, b.Module) + return err + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "part": partName, + }, bodyWriter, nil +} + +// WorkerPlainTextBinding is a binding to plain text. +// +// https://developers.cloudflare.com/workers/tooling/api/scripts/#add-a-plain-text-binding +type WorkerPlainTextBinding struct { + Text string +} + +// Type returns the type of the binding. +func (b WorkerPlainTextBinding) Type() WorkerBindingType { + return WorkerPlainTextBindingType +} + +func (b WorkerPlainTextBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.Text == "" { + return nil, nil, fmt.Errorf(`Text for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "text": b.Text, + }, nil, nil +} + +// WorkerSecretTextBinding is a binding to secret text. +// +// https://developers.cloudflare.com/workers/tooling/api/scripts/#add-a-secret-text-binding +type WorkerSecretTextBinding struct { + Text string +} + +// Type returns the type of the binding. +func (b WorkerSecretTextBinding) Type() WorkerBindingType { + return WorkerSecretTextBindingType +} + +func (b WorkerSecretTextBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.Text == "" { + return nil, nil, fmt.Errorf(`Text for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "text": b.Text, + }, nil, nil +} + +type WorkerServiceBinding struct { + Service string + Environment *string +} + +func (b WorkerServiceBinding) Type() WorkerBindingType { + return WorkerServiceBindingType +} + +func (b WorkerServiceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.Service == "" { + return nil, nil, fmt.Errorf(`Service for binding "%s" cannot be empty`, bindingName) + } + + meta := workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "service": b.Service, + } + + if b.Environment != nil { + meta["environment"] = *b.Environment + } + + return meta, nil, nil +} + +// WorkerR2BucketBinding is a binding to an R2 bucket. +type WorkerR2BucketBinding struct { + BucketName string +} + +// Type returns the type of the binding. +func (b WorkerR2BucketBinding) Type() WorkerBindingType { + return WorkerR2BucketBindingType +} + +func (b WorkerR2BucketBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.BucketName == "" { + return nil, nil, fmt.Errorf(`BucketName for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "bucket_name": b.BucketName, + }, nil, nil +} + +// WorkerAnalyticsEngineBinding is a binding to an Analytics Engine dataset. +type WorkerAnalyticsEngineBinding struct { + Dataset string +} + +// Type returns the type of the binding. +func (b WorkerAnalyticsEngineBinding) Type() WorkerBindingType { + return WorkerAnalyticsEngineBindingType +} + +func (b WorkerAnalyticsEngineBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) { + if b.Dataset == "" { + return nil, nil, fmt.Errorf(`Dataset for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": bindingName, + "type": b.Type(), + "dataset": b.Dataset, + }, nil, nil +} + +// WorkerQueueBinding is a binding to a Workers Queue. +// +// https://developers.cloudflare.com/workers/platform/bindings/#queue-bindings +type WorkerQueueBinding struct { + Binding string + Queue string +} + +// Type returns the type of the binding. +func (b WorkerQueueBinding) Type() WorkerBindingType { + return WorkerQueueBindingType +} + +func (b WorkerQueueBinding) 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.Queue == "" { + return nil, nil, fmt.Errorf(`Queue name for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "type": b.Type(), + "name": b.Binding, + "queue_name": b.Queue, + }, 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 { + randBytes := make([]byte, 16) + rand.Read(randBytes) //nolint:errcheck + return hex.EncodeToString(randBytes) +} + +// ListWorkerBindings returns all the bindings for a particular worker. +func (api *API) ListWorkerBindings(ctx context.Context, rc *ResourceContainer, params ListWorkerBindingsParams) (WorkerBindingListResponse, error) { + if params.ScriptName == "" { + return WorkerBindingListResponse{}, errors.New("ScriptName is required") + } + + if rc.Level != AccountRouteLevel { + return WorkerBindingListResponse{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return WorkerBindingListResponse{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings", rc.Identifier, params.ScriptName) + + var jsonRes struct { + Response + Bindings []workerBindingMeta `json:"result"` + } + var r WorkerBindingListResponse + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return r, err + } + err = json.Unmarshal(res, &jsonRes) + if err != nil { + return r, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + r = WorkerBindingListResponse{ + Response: jsonRes.Response, + BindingList: make([]WorkerBindingListItem, 0, len(jsonRes.Bindings)), + } + for _, jsonBinding := range jsonRes.Bindings { + name, ok := jsonBinding["name"].(string) + if !ok { + return r, fmt.Errorf("Binding missing name %v", jsonBinding) + } + bType, ok := jsonBinding["type"].(string) + if !ok { + return r, fmt.Errorf("Binding missing type %v", jsonBinding) + } + bindingListItem := WorkerBindingListItem{ + Name: name, + } + + switch WorkerBindingType(bType) { + case WorkerDurableObjectBindingType: + class_name := jsonBinding["class_name"].(string) + script_name := jsonBinding["script_name"].(string) + bindingListItem.Binding = WorkerDurableObjectBinding{ + ClassName: class_name, + ScriptName: script_name, + } + case WorkerKvNamespaceBindingType: + namespaceID := jsonBinding["namespace_id"].(string) + bindingListItem.Binding = WorkerKvNamespaceBinding{ + NamespaceID: namespaceID, + } + case WorkerQueueBindingType: + queueName := jsonBinding["queue_name"].(string) + bindingListItem.Binding = WorkerQueueBinding{ + Binding: name, + Queue: queueName, + } + case WorkerWebAssemblyBindingType: + bindingListItem.Binding = WorkerWebAssemblyBinding{ + Module: &bindingContentReader{ + api: api, + ctx: ctx, + accountID: rc.Identifier, + params: ¶ms, + bindingName: name, + }, + } + case WorkerPlainTextBindingType: + text := jsonBinding["text"].(string) + bindingListItem.Binding = WorkerPlainTextBinding{ + Text: text, + } + case WorkerServiceBindingType: + service := jsonBinding["service"].(string) + environment := jsonBinding["environment"].(string) + bindingListItem.Binding = WorkerServiceBinding{ + Service: service, + Environment: &environment, + } + case WorkerSecretTextBindingType: + bindingListItem.Binding = WorkerSecretTextBinding{} + case WorkerR2BucketBindingType: + bucketName := jsonBinding["bucket_name"].(string) + bindingListItem.Binding = WorkerR2BucketBinding{ + BucketName: bucketName, + } + case WorkerAnalyticsEngineBindingType: + dataset := jsonBinding["dataset"].(string) + bindingListItem.Binding = WorkerAnalyticsEngineBinding{ + Dataset: dataset, + } + default: + bindingListItem.Binding = WorkerInheritBinding{} + } + r.BindingList = append(r.BindingList, bindingListItem) + } + + return r, nil +} + +// bindingContentReader is an io.Reader that will lazily load the +// raw bytes for a binding from the API when the Read() method +// is first called. This is only useful for binding types +// that store raw bytes, like WebAssembly modules. +type bindingContentReader struct { + api *API + accountID string + params *ListWorkerBindingsParams + ctx context.Context + bindingName string + content []byte + position int +} + +func (b *bindingContentReader) Read(p []byte) (n int, err error) { + // Lazily load the content when Read() is first called + if b.content == nil { + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings/%s/content", b.accountID, b.params.ScriptName, b.bindingName) + res, err := b.api.makeRequestContext(b.ctx, http.MethodGet, uri, nil) + if err != nil { + return 0, err + } + b.content = res + } + + if b.position >= len(b.content) { + return 0, io.EOF + } + + bytesRemaining := len(b.content) - b.position + bytesToProcess := 0 + if len(p) < bytesRemaining { + bytesToProcess = len(p) + } else { + bytesToProcess = bytesRemaining + } + + for i := 0; i < bytesToProcess; i++ { + p[i] = b.content[b.position] + b.position = b.position + 1 + } + + return bytesToProcess, nil +} diff --git a/workers_bindings_test.go b/workers_bindings_test.go new file mode 100644 index 0000000000..f8dea25ad3 --- /dev/null +++ b/workers_bindings_test.go @@ -0,0 +1,97 @@ +package cloudflare + +import ( + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListWorkerBindings(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/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) + }) + + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/my-script/bindings/MY_WASM/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", "application/wasm") + _, _ = w.Write([]byte("mock multi-script wasm")) + }) + + res, err := client.ListWorkerBindings(context.Background(), AccountIdentifier(testAccountID), ListWorkerBindingsParams{ + ScriptName: "my-script", + }) + assert.NoError(t, err) + + assert.Equal(t, successResponse, res.Response) + assert.Equal(t, 8, 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()) + + assert.Equal(t, "MY_WASM", res.BindingList[1].Name) + wasmBinding := res.BindingList[1].Binding.(WorkerWebAssemblyBinding) + wasmModuleContent, err := io.ReadAll(wasmBinding.Module) + assert.NoError(t, err) + assert.Equal(t, []byte("mock multi-script wasm"), wasmModuleContent) + assert.Equal(t, WorkerWebAssemblyBindingType, res.BindingList[1].Binding.Type()) + + 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()) +} diff --git a/workers_cron_triggers.go b/workers_cron_triggers.go index 8938862aef..2322a5d682 100644 --- a/workers_cron_triggers.go +++ b/workers_cron_triggers.go @@ -27,12 +27,29 @@ type WorkerCronTrigger struct { ModifiedOn *time.Time `json:"modified_on,omitempty"` } +type ListWorkerCronTriggersParams struct { + ScriptName string +} + +type UpdateWorkerCronTriggersParams struct { + ScriptName string + Crons []WorkerCronTrigger +} + // 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, accountID, scriptName string) ([]WorkerCronTrigger, error) { - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", accountID, scriptName) +func (api *API) ListWorkerCronTriggers(ctx context.Context, rc *ResourceContainer, params ListWorkerCronTriggersParams) ([]WorkerCronTrigger, error) { + if rc.Level != AccountRouteLevel { + return []WorkerCronTrigger{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return []WorkerCronTrigger{}, ErrMissingIdentifier + } + + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", rc.Identifier, params.ScriptName) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return []WorkerCronTrigger{}, err @@ -49,9 +66,17 @@ func (api *API) ListWorkerCronTriggers(ctx context.Context, accountID, scriptNam // 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, accountID, scriptName string, crons []WorkerCronTrigger) ([]WorkerCronTrigger, error) { - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", accountID, scriptName) - res, err := api.makeRequestContext(ctx, http.MethodPut, uri, crons) +func (api *API) UpdateWorkerCronTriggers(ctx context.Context, rc *ResourceContainer, params UpdateWorkerCronTriggersParams) ([]WorkerCronTrigger, error) { + if rc.Level != AccountRouteLevel { + return []WorkerCronTrigger{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return []WorkerCronTrigger{}, ErrMissingIdentifier + } + + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", rc.Identifier, params.ScriptName) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params.Crons) if err != nil { return []WorkerCronTrigger{}, err } diff --git a/workers_cron_triggers_test.go b/workers_cron_triggers_test.go index d2acba1e25..0194d75ef3 100644 --- a/workers_cron_triggers_test.go +++ b/workers_cron_triggers_test.go @@ -11,7 +11,7 @@ import ( ) func TestListWorkerCronTriggers(t *testing.T) { - setup(UsingAccount("9a7806061c88ada191ed06f989cc3dac")) + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -42,14 +42,14 @@ func TestListWorkerCronTriggers(t *testing.T) { CreatedOn: &createdOn, }} - actual, err := client.ListWorkerCronTriggers(context.Background(), testAccountID, "example-script") + actual, err := client.ListWorkerCronTriggers(context.Background(), AccountIdentifier(testAccountID), ListWorkerCronTriggersParams{ScriptName: "example-script"}) if assert.NoError(t, err) { assert.Equal(t, want, actual) } } func TestUpdateWorkerCronTriggers(t *testing.T) { - setup(UsingAccount("9a7806061c88ada191ed06f989cc3dac")) + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -80,7 +80,7 @@ func TestUpdateWorkerCronTriggers(t *testing.T) { CreatedOn: &createdOn, }} - actual, err := client.UpdateWorkerCronTriggers(context.Background(), testAccountID, "example-script", want) + actual, err := client.UpdateWorkerCronTriggers(context.Background(), AccountIdentifier(testAccountID), UpdateWorkerCronTriggersParams{ScriptName: "example-script", Crons: want}) if assert.NoError(t, err) { assert.Equal(t, want, actual) } diff --git a/workers_example_test.go b/workers_example_test.go deleted file mode 100644 index 2206a35045..0000000000 --- a/workers_example_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package cloudflare_test - -import ( - "context" - "fmt" - "log" - - cloudflare "github.com/cloudflare/cloudflare-go" -) - -var ( - workerScript = "addEventListener('fetch', event => {\n event.passThroughOnException()\nevent.respondWith(handleRequest(event.request))\n})\n\nasync function handleRequest(request) {\n return fetch(request)\n}" -) - -func ExampleAPI_UploadWorker() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - - res, err := api.UploadWorker(context.Background(), &cloudflare.WorkerRequestParams{ZoneID: zoneID}, &cloudflare.WorkerScriptParams{Script: workerScript}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) - - UploadWorkerWithName() -} - -func UploadWorkerWithName() { - api, err := cloudflare.New(apiKey, user, cloudflare.UsingAccount("foo")) - if err != nil { - log.Fatal(err) - } - - res, err := api.UploadWorker(context.Background(), &cloudflare.WorkerRequestParams{ScriptName: "baz"}, &cloudflare.WorkerScriptParams{Script: workerScript}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} - -func ExampleAPI_DownloadWorker() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - - res, err := api.DownloadWorker(context.Background(), &cloudflare.WorkerRequestParams{ZoneID: zoneID}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) - - DownloadWorkerWithName() -} - -func DownloadWorkerWithName() { - api, err := cloudflare.New(apiKey, user, cloudflare.UsingAccount("foo")) - if err != nil { - log.Fatal(err) - } - - res, err := api.DownloadWorker(context.Background(), &cloudflare.WorkerRequestParams{ScriptName: "baz"}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} - -func ExampleAPI_DeleteWorker() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - res, err := api.DeleteWorker(context.Background(), &cloudflare.WorkerRequestParams{ZoneID: zoneID}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) - - DeleteWorkerWithName() -} - -func DeleteWorkerWithName() { - api, err := cloudflare.New(apiKey, user, cloudflare.UsingAccount("foo")) - if err != nil { - log.Fatal(err) - } - - res, err := api.DeleteWorker(context.Background(), &cloudflare.WorkerRequestParams{ScriptName: "baz"}) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} - -func ExampleAPI_ListWorkerScripts() { - api, err := cloudflare.New(apiKey, user, cloudflare.UsingAccount("foo")) - if err != nil { - log.Fatal(err) - } - - res, err := api.ListWorkerScripts(context.Background()) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res.WorkerList) -} - -func ExampleAPI_CreateWorkerRoute() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - route := cloudflare.WorkerRoute{Pattern: "app1.example.com/*", Enabled: true} - res, err := api.CreateWorkerRoute(context.Background(), zoneID, route) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} - -func ExampleAPI_UpdateWorkerRoute() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - // pull from existing list of routes to perform update on - routesResponse, err := api.ListWorkerRoutes(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - route := cloudflare.WorkerRoute{Pattern: "app2.example.com/*", Enabled: true} - // update first route retrieved from the listWorkerRoutes call with details above - res, err := api.UpdateWorkerRoute(context.Background(), zoneID, routesResponse.Routes[0].ID, route) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} - -func ExampleAPI_ListWorkerRoutes() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - res, err := api.ListWorkerRoutes(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} - -func ExampleAPI_DeleteWorkerRoute() { - api, err := cloudflare.New(apiKey, user) - if err != nil { - log.Fatal(err) - } - - zoneID, err := api.ZoneIDByName(domain) - if err != nil { - log.Fatal(err) - } - // pull from existing list of routes to perform delete on - routesResponse, err := api.ListWorkerRoutes(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - // delete first route retrieved from the listWorkerRoutes call - res, err := api.DeleteWorkerRoute(context.Background(), zoneID, routesResponse.Routes[0].ID) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v", res) -} diff --git a/workers_kv.go b/workers_kv.go index f7a01bc062..8a62924b63 100644 --- a/workers_kv.go +++ b/workers_kv.go @@ -106,6 +106,13 @@ type ListWorkersKVsParams struct { // // API reference: https://api.cloudflare.com/#workers-kv-namespace-create-a-namespace func (api *API) CreateWorkersKVNamespace(ctx context.Context, rc *ResourceContainer, params CreateWorkersKVNamespaceParams) (WorkersKVNamespaceResponse, error) { + if rc.Level != AccountRouteLevel { + return WorkersKVNamespaceResponse{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return WorkersKVNamespaceResponse{}, ErrMissingIdentifier + } uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces", rc.Identifier) res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) if err != nil { @@ -124,6 +131,14 @@ func (api *API) CreateWorkersKVNamespace(ctx context.Context, rc *ResourceContai // // API reference: https://api.cloudflare.com/#workers-kv-namespace-list-namespaces func (api *API) ListWorkersKVNamespaces(ctx context.Context, rc *ResourceContainer, params ListWorkersKVNamespacesParams) ([]WorkersKVNamespace, *ResultInfo, error) { + if rc.Level != AccountRouteLevel { + return []WorkersKVNamespace{}, &ResultInfo{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return []WorkersKVNamespace{}, &ResultInfo{}, ErrMissingIdentifier + } + autoPaginate := true if params.PerPage >= 1 || params.Page >= 1 { autoPaginate = false @@ -138,6 +153,7 @@ func (api *API) ListWorkersKVNamespaces(ctx context.Context, rc *ResourceContain var namespaces []WorkersKVNamespace var nsResponse ListWorkersKVNamespacesResponse for { + nsResponse = ListWorkersKVNamespacesResponse{} uri := buildURI(fmt.Sprintf("/accounts/%s/storage/kv/namespaces", rc.Identifier), params) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) @@ -183,6 +199,14 @@ func (api *API) DeleteWorkersKVNamespace(ctx context.Context, rc *ResourceContai // // API reference: https://api.cloudflare.com/#workers-kv-namespace-rename-a-namespace func (api *API) UpdateWorkersKVNamespace(ctx context.Context, rc *ResourceContainer, params UpdateWorkersKVNamespaceParams) (Response, error) { + if rc.Level != AccountRouteLevel { + return Response{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return Response{}, ErrMissingIdentifier + } + uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s", rc.Identifier, params.NamespaceID) res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) if err != nil { @@ -201,6 +225,14 @@ func (api *API) UpdateWorkersKVNamespace(ctx context.Context, rc *ResourceContai // // API reference: https://api.cloudflare.com/#workers-kv-namespace-write-key-value-pair func (api *API) WriteWorkersKVEntry(ctx context.Context, rc *ResourceContainer, params WriteWorkersKVEntryParams) (Response, error) { + if rc.Level != AccountRouteLevel { + return Response{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return Response{}, ErrMissingIdentifier + } + uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/values/%s", rc.Identifier, params.NamespaceID, url.PathEscape(params.Key)) res, err := api.makeRequestContextWithHeaders( ctx, http.MethodPut, uri, params.Value, http.Header{"Content-Type": []string{"application/octet-stream"}}, @@ -221,6 +253,14 @@ func (api *API) WriteWorkersKVEntry(ctx context.Context, rc *ResourceContainer, // // API reference: https://api.cloudflare.com/#workers-kv-namespace-write-multiple-key-value-pairs func (api *API) WriteWorkersKVEntries(ctx context.Context, rc *ResourceContainer, params WriteWorkersKVEntriesParams) (Response, error) { + if rc.Level != AccountRouteLevel { + return Response{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return Response{}, ErrMissingIdentifier + } + uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/bulk", rc.Identifier, params.NamespaceID) res, err := api.makeRequestContextWithHeaders( ctx, http.MethodPut, uri, params.KVs, http.Header{"Content-Type": []string{"application/json"}}, @@ -242,6 +282,13 @@ func (api *API) WriteWorkersKVEntries(ctx context.Context, rc *ResourceContainer // // API reference: https://api.cloudflare.com/#workers-kv-namespace-read-key-value-pair func (api API) GetWorkersKV(ctx context.Context, rc *ResourceContainer, params GetWorkersKVParams) ([]byte, error) { + if rc.Level != AccountRouteLevel { + return []byte(``), ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return []byte(``), ErrMissingIdentifier + } uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/values/%s", rc.Identifier, params.NamespaceID, url.PathEscape(params.Key)) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { @@ -254,6 +301,13 @@ func (api API) GetWorkersKV(ctx context.Context, rc *ResourceContainer, params G // // API reference: https://api.cloudflare.com/#workers-kv-namespace-delete-key-value-pair func (api API) DeleteWorkersKVEntry(ctx context.Context, rc *ResourceContainer, params DeleteWorkersKVEntryParams) (Response, error) { + if rc.Level != AccountRouteLevel { + return Response{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return Response{}, ErrMissingIdentifier + } uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/values/%s", rc.Identifier, params.NamespaceID, url.PathEscape(params.Key)) res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) if err != nil { @@ -271,6 +325,13 @@ func (api API) DeleteWorkersKVEntry(ctx context.Context, rc *ResourceContainer, // // API reference: https://api.cloudflare.com/#workers-kv-namespace-delete-multiple-key-value-pairs func (api *API) DeleteWorkersKVEntries(ctx context.Context, rc *ResourceContainer, params DeleteWorkersKVEntriesParams) (Response, error) { + if rc.Level != AccountRouteLevel { + return Response{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return Response{}, ErrMissingIdentifier + } uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/bulk", rc.Identifier, params.NamespaceID) res, err := api.makeRequestContextWithHeaders( ctx, http.MethodDelete, uri, params.Keys, http.Header{"Content-Type": []string{"application/json"}}, @@ -291,6 +352,14 @@ func (api *API) DeleteWorkersKVEntries(ctx context.Context, rc *ResourceContaine // // API Reference: https://api.cloudflare.com/#workers-kv-namespace-list-a-namespace-s-keys func (api API) ListWorkersKVKeys(ctx context.Context, rc *ResourceContainer, params ListWorkersKVsParams) (ListStorageKeysResponse, error) { + if rc.Level != AccountRouteLevel { + return ListStorageKeysResponse{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return ListStorageKeysResponse{}, ErrMissingIdentifier + } + uri := buildURI( fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/keys", rc.Identifier, params.NamespaceID), params, diff --git a/workers_kv_test.go b/workers_kv_test.go index 1ad7ae091d..80d7b140b9 100644 --- a/workers_kv_test.go +++ b/workers_kv_test.go @@ -29,7 +29,7 @@ func TestWorkersKV_CreateWorkersKVNamespace(t *testing.T) { mux.HandleFunc("/accounts/"+testAccountID+"/storage/kv/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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) res, err := client.CreateWorkersKVNamespace(context.Background(), AccountIdentifier(testAccountID), CreateWorkersKVNamespaceParams{Title: "Namespace"}) @@ -60,7 +60,7 @@ func TestWorkersKV_DeleteWorkersKVNamespace(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s", namespace), 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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) res, err := client.DeleteWorkersKVNamespace(context.Background(), AccountIdentifier(testAccountID), namespace) @@ -99,7 +99,7 @@ func TestWorkersKV_ListWorkersKVNamespaces(t *testing.T) { mux.HandleFunc("/accounts/"+testAccountID+"/storage/kv/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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) res, _, err := client.ListWorkersKVNamespaces(context.Background(), AccountIdentifier(testAccountID), ListWorkersKVNamespacesParams{}) @@ -170,10 +170,10 @@ func TestWorkersKV_ListWorkersKVNamespaceMultiplePages(t *testing.T) { w.Header().Set("content-type", "application/javascript") if r.URL.Query().Get("page") == "1" { - fmt.Fprintf(w, response1) //nolint + fmt.Fprint(w, response1) return } else if r.URL.Query().Get("page") == "2" { - fmt.Fprintf(w, response2) //nolint + fmt.Fprint(w, response2) return } else { panic(errors.New("Got a request for an unexpected page")) @@ -218,7 +218,7 @@ func TestWorkersKV_UpdateWorkersKVNamespace(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s", namespace), func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) res, err := client.UpdateWorkersKVNamespace(context.Background(), AccountIdentifier(testAccountID), UpdateWorkersKVNamespaceParams{Title: "Namespace", NamespaceID: namespace}) @@ -246,7 +246,7 @@ func TestWorkersKV_WriteWorkersKVEntry(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/values/%s", namespace, key), func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/octet-stream") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) want := successResponse @@ -278,7 +278,7 @@ func TestWorkersKV_WriteWorkersKVEntries(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/bulk", namespace), func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) want := successResponse @@ -297,7 +297,7 @@ func TestWorkersKV_ReadWorkersKV(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/values/%s", namespace, key), 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", "text/plain") - fmt.Fprintf(w, "test_value") + fmt.Fprint(w, "test_value") }) res, err := client.GetWorkersKV(context.Background(), AccountIdentifier(testAccountID), GetWorkersKVParams{NamespaceID: namespace, Key: key}) @@ -324,7 +324,7 @@ func TestWorkersKV_DeleteWorkersKVEntry(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/values/%s", namespace, key), 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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) res, err := client.DeleteWorkersKVEntry(context.Background(), AccountIdentifier(testAccountID), DeleteWorkersKVEntryParams{NamespaceID: namespace, Key: key}) @@ -352,7 +352,7 @@ func TestWorkersKV_DeleteWorkersKVBulk(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/bulk", namespace), 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.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) want := successResponse @@ -386,7 +386,7 @@ func TestWorkersKV_ListKeys(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/keys", namespace), 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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) res, err := client.ListWorkersKVKeys(context.Background(), AccountIdentifier(testAccountID), ListWorkersKVsParams{NamespaceID: namespace}) @@ -456,7 +456,7 @@ func TestWorkersKV_ListKeysWithParameters(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/accounts/"+testAccountID+"/storage/kv/namespaces/%s/keys", namespace), 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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) limit, prefix := 25, "test-prefix" diff --git a/workers_routes.go b/workers_routes.go new file mode 100644 index 0000000000..d6dc1c2a71 --- /dev/null +++ b/workers_routes.go @@ -0,0 +1,161 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +var ErrMissingWorkerRouteID = errors.New("missing required route ID") + +type ListWorkerRoutes struct{} + +type CreateWorkerRouteParams struct { + Pattern string `json:"pattern"` + Script string `json:"script,omitempty"` +} + +type ListWorkerRoutesParams struct{} + +type UpdateWorkerRouteParams struct { + ID string `json:"id,omitempty"` + Pattern string `json:"pattern"` + Script string `json:"script,omitempty"` +} + +// CreateWorkerRoute creates worker route for a script. +// +// API reference: https://api.cloudflare.com/#worker-routes-create-route +func (api *API) CreateWorkerRoute(ctx context.Context, rc *ResourceContainer, params CreateWorkerRouteParams) (WorkerRouteResponse, error) { + if rc.Level != ZoneRouteLevel { + return WorkerRouteResponse{}, fmt.Errorf(errInvalidResourceContainerAccess, ZoneRouteLevel) + } + + if rc.Identifier == "" { + return WorkerRouteResponse{}, ErrMissingIdentifier + } + + uri := fmt.Sprintf("/zones/%s/workers/routes", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return WorkerRouteResponse{}, err + } + + var r WorkerRouteResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r, nil +} + +// DeleteWorkerRoute deletes worker route for a script. +// +// API reference: https://api.cloudflare.com/#worker-routes-delete-route +func (api *API) DeleteWorkerRoute(ctx context.Context, rc *ResourceContainer, routeID string) (WorkerRouteResponse, error) { + if rc.Level != ZoneRouteLevel { + return WorkerRouteResponse{}, fmt.Errorf(errInvalidResourceContainerAccess, ZoneRouteLevel) + } + + if rc.Identifier == "" { + return WorkerRouteResponse{}, ErrMissingIdentifier + } + + if routeID == "" { + return WorkerRouteResponse{}, errors.New("missing required route ID") + } + + uri := fmt.Sprintf("/zones/%s/workers/routes/%s", rc.Identifier, routeID) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return WorkerRouteResponse{}, err + } + var r WorkerRouteResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r, nil +} + +// ListWorkerRoutes returns list of Worker routes. +// +// API reference: https://api.cloudflare.com/#worker-routes-list-routes +func (api *API) ListWorkerRoutes(ctx context.Context, rc *ResourceContainer, params ListWorkerRoutesParams) (WorkerRoutesResponse, error) { + if rc.Level != ZoneRouteLevel { + return WorkerRoutesResponse{}, fmt.Errorf(errInvalidResourceContainerAccess, ZoneRouteLevel) + } + + if rc.Identifier == "" { + return WorkerRoutesResponse{}, ErrMissingIdentifier + } + + uri := fmt.Sprintf("/zones/%s/workers/routes", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return WorkerRoutesResponse{}, err + } + var r WorkerRoutesResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WorkerRoutesResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r, nil +} + +// GetWorkerRoute returns a Workers route. +// +// API reference: https://api.cloudflare.com/#worker-routes-get-route +func (api *API) GetWorkerRoute(ctx context.Context, rc *ResourceContainer, routeID string) (WorkerRouteResponse, error) { + if rc.Level != ZoneRouteLevel { + return WorkerRouteResponse{}, fmt.Errorf(errInvalidResourceContainerAccess, ZoneRouteLevel) + } + + if rc.Identifier == "" { + return WorkerRouteResponse{}, ErrMissingIdentifier + } + + uri := fmt.Sprintf("/zones/%s/workers/routes/%s", rc.Identifier, routeID) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return WorkerRouteResponse{}, err + } + var r WorkerRouteResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r, nil +} + +// UpdateWorkerRoute updates worker route for a script. +// +// API reference: https://api.cloudflare.com/#worker-routes-update-route +func (api *API) UpdateWorkerRoute(ctx context.Context, rc *ResourceContainer, params UpdateWorkerRouteParams) (WorkerRouteResponse, error) { + if rc.Level != ZoneRouteLevel { + return WorkerRouteResponse{}, fmt.Errorf(errInvalidResourceContainerAccess, ZoneRouteLevel) + } + + if rc.Identifier == "" { + return WorkerRouteResponse{}, ErrMissingIdentifier + } + + if params.ID == "" { + return WorkerRouteResponse{}, ErrMissingWorkerRouteID + } + + uri := fmt.Sprintf("/zones/%s/workers/routes/%s", rc.Identifier, params.ID) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return WorkerRouteResponse{}, err + } + var r WorkerRouteResponse + err = json.Unmarshal(res, &r) + if err != nil { + return WorkerRouteResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r, nil +} diff --git a/workers_routes_test.go b/workers_routes_test.go new file mode 100644 index 0000000000..8d36fdc9d4 --- /dev/null +++ b/workers_routes_test.go @@ -0,0 +1,121 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateWorkersRoute(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/"+testZoneID+"/workers/routes", 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, createWorkerRouteResponse) + }) + + res, err := client.CreateWorkerRoute(context.Background(), ZoneIdentifier(testZoneID), CreateWorkerRouteParams{ + Pattern: "app1.example.com/*", + Script: "example", + }) + + want := WorkerRouteResponse{successResponse, WorkerRoute{ID: "e7a57d8746e74ae49c25994dadb421b1"}} + if assert.NoError(t, err) { + assert.Equal(t, want, res) + } +} + +func TestDeleteWorkersRoute(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/"+testZoneID+"/workers/routes/e7a57d8746e74ae49c25994dadb421b1", 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, deleteWorkerRouteResponseData) + }) + res, err := client.DeleteWorkerRoute(context.Background(), ZoneIdentifier(testZoneID), "e7a57d8746e74ae49c25994dadb421b1") + want := WorkerRouteResponse{successResponse, + WorkerRoute{ + ID: "e7a57d8746e74ae49c25994dadb421b1", + }} + if assert.NoError(t, err) { + assert.Equal(t, want, res) + } +} + +func TestListWorkersRoute(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/"+testZoneID+"/workers/routes", 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, listWorkerRouteResponse) + }) + + res, err := client.ListWorkerRoutes(context.Background(), ZoneIdentifier(testZoneID), ListWorkerRoutesParams{}) + want := WorkerRoutesResponse{successResponse, + []WorkerRoute{ + {ID: "e7a57d8746e74ae49c25994dadb421b1", Pattern: "app1.example.com/*", ScriptName: "test_script_1"}, + {ID: "f8b68e9857f85bf59c25994dadb421b1", Pattern: "app2.example.com/*", ScriptName: "test_script_2"}, + {ID: "2b5bf4240cd34c77852fac70b1bf745a", Pattern: "app3.example.com/*", ScriptName: "test_script_3"}, + }, + } + if assert.NoError(t, err) { + assert.Equal(t, want, res) + } +} + +func TestGetWorkersRoute(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/"+testZoneID+"/workers/routes/1234", 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, getRouteResponseData) + }) + + res, err := client.GetWorkerRoute(context.Background(), ZoneIdentifier(testZoneID), "1234") + want := WorkerRouteResponse{successResponse, + WorkerRoute{ + ID: "e7a57d8746e74ae49c25994dadb421b1", + Pattern: "app1.example.com/*", + ScriptName: "script-name"}, + } + if assert.NoError(t, err) { + assert.Equal(t, want, res) + } +} + +func TestUpdateWorkersRoute(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/zones/"+testZoneID+"/workers/routes/e7a57d8746e74ae49c25994dadb421b1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, updateWorkerRouteEntResponse) + }) + + res, err := client.UpdateWorkerRoute(context.Background(), ZoneIdentifier(testZoneID), UpdateWorkerRouteParams{ + ID: "e7a57d8746e74ae49c25994dadb421b1", + Pattern: "app3.example.com/*", + Script: "test_script_1", + }) + want := WorkerRouteResponse{successResponse, + WorkerRoute{ + ID: "e7a57d8746e74ae49c25994dadb421b1", + Pattern: "app3.example.com/*", + ScriptName: "test_script_1", + }} + if assert.NoError(t, err) { + assert.Equal(t, want, res) + } +} diff --git a/workers_secrets.go b/workers_secrets.go index 09d8ba337e..458cd1a7c4 100644 --- a/workers_secrets.go +++ b/workers_secrets.go @@ -32,11 +32,34 @@ type WorkersListSecretsResponse struct { Result []WorkersSecret `json:"result"` } -// SetWorkersSecret creates or updates a secret +type SetWorkersSecretParams struct { + ScriptName string + Secret *WorkersPutSecretRequest +} + +type DeleteWorkersSecretParams struct { + ScriptName string + SecretName string +} + +type ListWorkersSecretsParams struct { + ScriptName string +} + +// SetWorkersSecret creates or updates a secret. +// // API reference: https://api.cloudflare.com/ -func (api *API) SetWorkersSecret(ctx context.Context, script string, req *WorkersPutSecretRequest) (WorkersPutSecretResponse, error) { - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/secrets", api.AccountID, script) - res, err := api.makeRequestContext(ctx, http.MethodPut, uri, req) +func (api *API) SetWorkersSecret(ctx context.Context, rc *ResourceContainer, params SetWorkersSecretParams) (WorkersPutSecretResponse, error) { + if rc.Level != AccountRouteLevel { + return WorkersPutSecretResponse{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return WorkersPutSecretResponse{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/secrets", rc.Identifier, params.ScriptName) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params.Secret) if err != nil { return WorkersPutSecretResponse{}, err } @@ -49,10 +72,19 @@ func (api *API) SetWorkersSecret(ctx context.Context, script string, req *Worker return result, err } -// DeleteWorkersSecret deletes a secret +// DeleteWorkersSecret deletes a secret. +// // API reference: https://api.cloudflare.com/ -func (api *API) DeleteWorkersSecret(ctx context.Context, script, secretName string) (Response, error) { - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/secrets/%s", api.AccountID, script, secretName) +func (api *API) DeleteWorkersSecret(ctx context.Context, rc *ResourceContainer, params DeleteWorkersSecretParams) (Response, error) { + if rc.Level != AccountRouteLevel { + return Response{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return Response{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/secrets/%s", rc.Identifier, params.ScriptName, params.SecretName) res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) if err != nil { return Response{}, err @@ -68,8 +100,16 @@ func (api *API) DeleteWorkersSecret(ctx context.Context, script, secretName stri // ListWorkersSecrets lists secrets for a given worker // API reference: https://api.cloudflare.com/ -func (api *API) ListWorkersSecrets(ctx context.Context, script string) (WorkersListSecretsResponse, error) { - uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/secrets", api.AccountID, script) +func (api *API) ListWorkersSecrets(ctx context.Context, rc *ResourceContainer, params ListWorkersSecretsParams) (WorkersListSecretsResponse, error) { + if rc.Level != AccountRouteLevel { + return WorkersListSecretsResponse{}, ErrRequiredAccountLevelResourceContainer + } + + if rc.Identifier == "" { + return WorkersListSecretsResponse{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/secrets", rc.Identifier, params.ScriptName) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return WorkersListSecretsResponse{}, err diff --git a/workers_secrets_test.go b/workers_secrets_test.go index c08ef71dd2..dc3ac99faa 100644 --- a/workers_secrets_test.go +++ b/workers_secrets_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" ) -func TestWorkers_SetWorkersSecret(t *testing.T) { - setup(UsingAccount("foo")) +func TestSetWorkersSecret(t *testing.T) { + setup() defer teardown() response := `{ @@ -23,16 +23,16 @@ func TestWorkers_SetWorkersSecret(t *testing.T) { "messages": [] }` - mux.HandleFunc("/accounts/foo/workers/scripts/test-script/secrets", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/test-script/secrets", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) req := &WorkersPutSecretRequest{ Name: "my-secret", Text: "super-secret", } - res, err := client.SetWorkersSecret(context.Background(), "test-script", req) + res, err := client.SetWorkersSecret(context.Background(), AccountIdentifier(testAccountID), SetWorkersSecretParams{ScriptName: "test-script", Secret: req}) want := WorkersPutSecretResponse{ successResponse, WorkersSecret{ @@ -46,8 +46,8 @@ func TestWorkers_SetWorkersSecret(t *testing.T) { } } -func TestWorkers_DeleteWorkersSecret(t *testing.T) { - setup(UsingAccount("foo")) +func TestDeleteWorkersSecret(t *testing.T) { + setup() defer teardown() response := `{ @@ -60,13 +60,13 @@ func TestWorkers_DeleteWorkersSecret(t *testing.T) { "messages": [] }` - mux.HandleFunc("/accounts/foo/workers/scripts/test-script/secrets/my-secret", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/test-script/secrets/my-secret", 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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) - res, err := client.DeleteWorkersSecret(context.Background(), "test-script", "my-secret") + res, err := client.DeleteWorkersSecret(context.Background(), AccountIdentifier(testAccountID), DeleteWorkersSecretParams{ScriptName: "test-script", SecretName: "my-secret"}) want := successResponse if assert.NoError(t, err) { @@ -74,8 +74,8 @@ func TestWorkers_DeleteWorkersSecret(t *testing.T) { } } -func TestWorkers_ListWorkersSecret(t *testing.T) { - setup(UsingAccount("foo")) +func TestListWorkersSecret(t *testing.T) { + setup() defer teardown() response := `{ @@ -88,13 +88,13 @@ func TestWorkers_ListWorkersSecret(t *testing.T) { "messages": [] }` - mux.HandleFunc("/accounts/foo/workers/scripts/test-script/secrets", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/test-script/secrets", 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/javascript") - fmt.Fprintf(w, response) //nolint + fmt.Fprint(w, response) }) - res, err := client.ListWorkersSecrets(context.Background(), "test-script") + res, err := client.ListWorkersSecrets(context.Background(), AccountIdentifier(testAccountID), ListWorkersSecretsParams{ScriptName: "test-script"}) want := WorkersListSecretsResponse{ successResponse, []WorkersSecret{ diff --git a/workers_tail.go b/workers_tail.go index 478112b4a0..0f074b0b66 100644 --- a/workers_tail.go +++ b/workers_tail.go @@ -35,13 +35,14 @@ type ListWorkersTailResponse struct { Result WorkersTail } -// StartWorkersTail Starts a tail that receives logs and exception from a Worker +// StartWorkersTail Starts a tail that receives logs and exception from a Worker. // // API reference: https://api.cloudflare.com/#worker-tail-logs-start-tail func (api *API) StartWorkersTail(ctx context.Context, rc *ResourceContainer, scriptName string) (WorkersTail, error) { if rc.Identifier == "" { return WorkersTail{}, ErrMissingAccountID } + if scriptName == "" { return WorkersTail{}, ErrMissingScriptName } @@ -60,7 +61,7 @@ func (api *API) StartWorkersTail(ctx context.Context, rc *ResourceContainer, scr return workerstailResponse.Result, nil } -// ListWorkersTail Get list of tails currently deployed on a worker +// ListWorkersTail Get list of tails currently deployed on a Worker. // // API reference: https://api.cloudflare.com/#worker-tail-logs-list-tails func (api *API) ListWorkersTail(ctx context.Context, rc *ResourceContainer, params ListWorkersTailParameters) (WorkersTail, error) { @@ -86,7 +87,7 @@ func (api *API) ListWorkersTail(ctx context.Context, rc *ResourceContainer, para return workerstailResponse.Result, nil } -// DeleteWorkersTail Deletes a tail from a Worker +// DeleteWorkersTail Deletes a tail from a Worker. // // API reference: https://api.cloudflare.com/#worker-tail-logs-delete-tail func (api *API) DeleteWorkersTail(ctx context.Context, rc *ResourceContainer, scriptName, tailID string) error { diff --git a/workers_test.go b/workers_test.go index 1f5dbfbc08..8492cdee06 100644 --- a/workers_test.go +++ b/workers_test.go @@ -89,7 +89,7 @@ const ( "errors": [], "messages": [] }` - listRouteEntResponseData = `{ + listWorkerRouteResponse = `{ "result": [ { "id": "e7a57d8746e74ae49c25994dadb421b1", @@ -103,7 +103,8 @@ const ( }, { "id": "2b5bf4240cd34c77852fac70b1bf745a", - "pattern": "app3.example.com/*" + "pattern": "app3.example.com/*", + "script": "test_script_3" } ], "success": true, @@ -210,6 +211,18 @@ export default { } --workermodulescriptdownload-- ` + uploadModuleWorkerSmartPlacement = `{ + "result": { + "script": "export default {\n async fetch(request, env, event) {\n event.passThroughOnException()\n return fetch(request)\n }\n}", + "etag": "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a", + "size": 191, + "modified_on": "2018-06-09T15:17:01.989141Z", + "placement_mode": "smart" + }, + "success": true, + "errors": [], + "messages": [] +}` ) var ( @@ -274,8 +287,12 @@ func getFileDetails(r *http.Request, key string) (*multipart.FileHeader, error) } type multipartUpload struct { - Script string - BindingMeta map[string]workerBindingMeta + Script string + BindingMeta map[string]workerBindingMeta + Logpush *bool + CompatibilityDate string + CompatibilityFlags []string + Placement *Placement } func parseMultipartUpload(r *http.Request) (multipartUpload, error) { @@ -286,9 +303,13 @@ func parseMultipartUpload(r *http.Request) (multipartUpload, error) { } var metadata struct { - BodyPart string `json:"body_part,omitempty"` - MainModule string `json:"main_module,omitempty"` - Bindings []workerBindingMeta `json:"bindings"` + BodyPart string `json:"body_part,omitempty"` + MainModule string `json:"main_module,omitempty"` + Bindings []workerBindingMeta `json:"bindings"` + Logpush *bool `json:"logpush,omitempty"` + CompatibilityDate string `json:"compatibility_date,omitempty"` + CompatibilityFlags []string `json:"compatibility_flags,omitempty"` + Placement *Placement `json:"placement,omitempty"` } err = json.Unmarshal(mdBytes, &metadata) if err != nil { @@ -315,66 +336,39 @@ func parseMultipartUpload(r *http.Request) (multipartUpload, error) { } return multipartUpload{ - Script: string(script), - BindingMeta: bindingMeta, + Script: string(script), + BindingMeta: bindingMeta, + Logpush: metadata.Logpush, + CompatibilityDate: metadata.CompatibilityDate, + CompatibilityFlags: metadata.CompatibilityFlags, + Placement: metadata.Placement, }, nil } -func TestWorkers_DeleteWorker(t *testing.T) { +func TestDeleteWorker(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/script", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", 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/javascript") - fmt.Fprintf(w, deleteWorkerResponseData) //nolint + fmt.Fprint(w, deleteWorkerResponseData) }) - res, err := client.DeleteWorker(context.Background(), &WorkerRequestParams{ZoneID: "foo"}) - want := WorkerScriptResponse{ - Response: successResponse, - } - if assert.NoError(t, err) { - assert.Equal(t, want.Response, res.Response) - } -} - -func TestWorkers_DeleteWorkerWithName(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/accounts/foo/workers/scripts/bar", 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/javascript") - fmt.Fprintf(w, deleteWorkerResponseData) //nolint - }) - res, err := client.DeleteWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}) - want := WorkerScriptResponse{ - Response: successResponse, - } - if assert.NoError(t, err) { - assert.Equal(t, want.Response, res.Response) - } -} - -func TestWorkers_DeleteWorkerWithNameErrorsWithoutAccountId(t *testing.T) { - setup() - defer teardown() - - _, err := client.DeleteWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}) - assert.Error(t, err) + err := client.DeleteWorker(context.Background(), AccountIdentifier(testAccountID), DeleteWorkerParams{ScriptName: "bar"}) + assert.NoError(t, err) } -func TestWorkers_DownloadWorker(t *testing.T) { +func TestGetWorker(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/script", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", 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/javascript") - fmt.Fprintf(w, workerScript) //nolint + fmt.Fprint(w, workerScript) }) - res, err := client.DownloadWorker(context.Background(), &WorkerRequestParams{ZoneID: "foo"}) + res, err := client.GetWorker(context.Background(), AccountIdentifier(testAccountID), "foo") want := WorkerScriptResponse{ successResponse, false, @@ -386,67 +380,41 @@ func TestWorkers_DownloadWorker(t *testing.T) { } } -func TestWorkers_DownloadWorkerWithName(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/accounts/foo/workers/scripts/bar", 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/javascript") - fmt.Fprintf(w, workerScript) //nolint - }) - res, err := client.DownloadWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}) - want := WorkerScriptResponse{ - successResponse, - false, - WorkerScript{ - Script: workerScript, - }} - if assert.NoError(t, err) { - assert.Equal(t, want.Script, res.Script) - } -} - -func TestWorkers_DownloadWorkerWithNameErrorsWithoutAccountId(t *testing.T) { +func TestGetWorker_Module(t *testing.T) { setup() defer teardown() - _, err := client.DownloadWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}) - assert.Error(t, err) -} - -func TestWorkers_DownloadWorkerModule(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/accounts/foo/workers/scripts/bar", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", 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.Fprintf(w, workerModuleScriptDownloadResponse) //nolint + fmt.Fprint(w, workerModuleScriptDownloadResponse) }) - res, err := client.DownloadWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}) + + res, err := client.GetWorker(context.Background(), AccountIdentifier(testAccountID), "foo") want := WorkerScriptResponse{ successResponse, true, WorkerScript{ Script: workerModuleScript, - }} + }, + } + if assert.NoError(t, err) { assert.Equal(t, want.Script, res.Script) } } -func TestWorkers_ListWorkerScripts(t *testing.T) { - setup(UsingAccount("foo")) +func TestListWorkers(t *testing.T) { + setup() defer teardown() - mux.HandleFunc("/accounts/foo/workers/scripts", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts", 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.Fprintf(w, listWorkersResponseData) //nolint + fmt.Fprint(w, listWorkersResponseData) }) - res, err := client.ListWorkerScripts(context.Background()) + res, _, err := client.ListWorkers(context.Background(), AccountIdentifier(testAccountID), ListWorkersParams{}) sampleDate, _ := time.Parse(time.RFC3339Nano, "2018-04-22T17:10:48.938097Z") want := []WorkerMetaData{ { @@ -467,18 +435,18 @@ func TestWorkers_ListWorkerScripts(t *testing.T) { } } -func TestWorkers_UploadWorker(t *testing.T) { +func TestUploadWorker_Basic(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/script", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) contentTypeHeader := r.Header.Get("content-type") assert.Equal(t, "application/javascript", contentTypeHeader, "Expected content-type request header to be 'application/javascript', got %s", contentTypeHeader) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) }) - res, err := client.UploadWorker(context.Background(), &WorkerRequestParams{ZoneID: "foo"}, &WorkerScriptParams{Script: workerScript}) + res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerScript}) formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z") want := WorkerScriptResponse{ successResponse, @@ -496,11 +464,11 @@ func TestWorkers_UploadWorker(t *testing.T) { } } -func TestWorkers_UploadWorkerAsModule(t *testing.T) { +func TestUploadWorker_Module(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/script", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", func(w http.ResponseWriter, r *http.Request) { mpUpload, err := parseMultipartUpload(r) assert.NoError(t, err) @@ -515,9 +483,9 @@ func TestWorkers_UploadWorkerAsModule(t *testing.T) { assert.Equal(t, expectedContentType, contentTypeHeader, "Expected content-type request header to be %s, got %s", expectedContentType, contentTypeHeader) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerModuleResponseData) //nolint + fmt.Fprint(w, uploadWorkerModuleResponseData) }) - res, err := client.UploadWorker(context.Background(), &WorkerRequestParams{ZoneID: "foo"}, &WorkerScriptParams{Script: workerModuleScript, Module: true}) + res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerModuleScript, Module: true}) formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z") want := WorkerScriptResponse{ successResponse, @@ -535,76 +503,10 @@ func TestWorkers_UploadWorkerAsModule(t *testing.T) { } } -func TestWorkers_UploadWorkerWithName(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/accounts/foo/workers/scripts/bar", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) - contentTypeHeader := r.Header.Get("content-type") - assert.Equal(t, "application/javascript", contentTypeHeader, "Expected content-type request header to be 'application/javascript', got %s", contentTypeHeader) - w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint - }) - res, err := client.UploadWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &WorkerScriptParams{Script: workerScript}) - formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z") - want := WorkerScriptResponse{ - successResponse, - false, - WorkerScript{ - Script: workerScript, - WorkerMetaData: WorkerMetaData{ - ETAG: "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a", - Size: 191, - ModifiedOn: formattedTime, - }, - }} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} - -func TestWorkers_UploadWorkerSingleScriptWithAccount(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/zones/foo/workers/script", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) - contentTypeHeader := r.Header.Get("content-type") - assert.Equal(t, "application/javascript", contentTypeHeader, "Expected content-type request header to be 'application/javascript', got %s", contentTypeHeader) - w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint - }) - res, err := client.UploadWorker(context.Background(), &WorkerRequestParams{ZoneID: "foo"}, &WorkerScriptParams{Script: workerScript}) - formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z") - want := WorkerScriptResponse{ - successResponse, - false, - WorkerScript{ - Script: workerScript, - WorkerMetaData: WorkerMetaData{ - ETAG: "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a", - Size: 191, - ModifiedOn: formattedTime, - }, - }} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} - -func TestWorkers_UploadWorkerWithNameErrorsWithoutAccountId(t *testing.T) { +func TestUploadWorker_WithDurableObjectBinding(t *testing.T) { setup() defer teardown() - _, err := client.UploadWorker(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &WorkerScriptParams{Script: workerScript}) - assert.Error(t, err) -} - -func TestWorkers_UploadWorkerWithDurableObjectBinding(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) @@ -623,25 +525,27 @@ func TestWorkers_UploadWorkerWithDurableObjectBinding(t *testing.T) { assert.Equal(t, expectedBindings, mpUpload.BindingMeta) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) - scriptParams := WorkerScriptParams{ - Script: workerScript, + 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": WorkerDurableObjectBinding{ ClassName: "TheClass", ScriptName: "the_script", }, }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + }) + assert.NoError(t, err) } -func TestWorkers_UploadWorkerWithInheritBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithInheritBinding(t *testing.T) { + setup() defer teardown() // Setup route handler for both single-script and multi-script @@ -666,22 +570,10 @@ func TestWorkers_UploadWorkerWithInheritBinding(t *testing.T) { assert.Equal(t, expectedBindings, mpUpload.BindingMeta) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/zones/foo/workers/script", handler) - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - scriptParams := WorkerScriptParams{ - Script: workerScript, - Bindings: map[string]WorkerBinding{ - "b1": WorkerInheritBinding{}, - "b2": WorkerInheritBinding{ - OldName: "old_binding_name", - }, - }, - } - - // Expected response formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z") want := WorkerScriptResponse{ successResponse, @@ -695,21 +587,22 @@ func TestWorkers_UploadWorkerWithInheritBinding(t *testing.T) { }, }} - // Test single-script - res, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ZoneID: "foo"}, &scriptParams) - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } - - // Test multi-script - res, err = client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, + Bindings: map[string]WorkerBinding{ + "b1": WorkerInheritBinding{}, + "b2": WorkerInheritBinding{ + OldName: "old_binding_name", + }, + }}) if assert.NoError(t, err) { assert.Equal(t, want, res) } } -func TestWorkers_UploadWorkerWithKVBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithKVBinding(t *testing.T) { + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -729,24 +622,23 @@ func TestWorkers_UploadWorkerWithKVBinding(t *testing.T) { assert.Equal(t, expectedBindings, mpUpload.BindingMeta) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - scriptParams := WorkerScriptParams{ - Script: workerScript, + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, Bindings: map[string]WorkerBinding{ "b1": WorkerKvNamespaceBinding{ NamespaceID: "test-namespace", }, - }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + }}) assert.NoError(t, err) } -func TestWorkers_UploadWorkerWithWasmBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithWasmBinding(t *testing.T) { + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -771,24 +663,25 @@ func TestWorkers_UploadWorkerWithWasmBinding(t *testing.T) { assert.Equal(t, []byte("fake-wasm"), wasmContent) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - scriptParams := WorkerScriptParams{ - Script: workerScript, + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, Bindings: map[string]WorkerBinding{ "b1": WorkerWebAssemblyBinding{ Module: strings.NewReader("fake-wasm"), }, }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + }) + assert.NoError(t, err) } -func TestWorkers_UploadWorkerWithPlainTextBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithPlainTextBinding(t *testing.T) { + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -808,27 +701,28 @@ func TestWorkers_UploadWorkerWithPlainTextBinding(t *testing.T) { assert.Equal(t, expectedBindings, mpUpload.BindingMeta) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - scriptParams := WorkerScriptParams{ - Script: workerScript, + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, Bindings: map[string]WorkerBinding{ "b1": WorkerPlainTextBinding{ Text: "plain text value", }, }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + }) + assert.NoError(t, err) } -func TestWorkers_UploadWorkerAsModuleWithPlainTextBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_ModuleWithPlainTextBinding(t *testing.T) { + setup() defer teardown() - mux.HandleFunc("/accounts/foo/workers/scripts/bar", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", 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) @@ -853,24 +747,25 @@ func TestWorkers_UploadWorkerAsModuleWithPlainTextBinding(t *testing.T) { assert.Equal(t, expectedContentDisposition, contentDispositonHeader, "Expected content-disposition request header to be %s, got %s", expectedContentDisposition, contentDispositonHeader) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerModuleResponseData) //nolint + fmt.Fprint(w, uploadWorkerModuleResponseData) }) - scriptParams := WorkerScriptParams{ - Script: workerModuleScript, - Module: true, + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerModuleScript, + Module: true, Bindings: map[string]WorkerBinding{ "b1": WorkerPlainTextBinding{ Text: "plain text value", }, }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + }) + assert.NoError(t, err) } -func TestWorkers_UploadWorkerWithSecretTextBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithSecretTextBinding(t *testing.T) { + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -890,24 +785,24 @@ func TestWorkers_UploadWorkerWithSecretTextBinding(t *testing.T) { assert.Equal(t, expectedBindings, mpUpload.BindingMeta) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - scriptParams := WorkerScriptParams{ - Script: workerScript, + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, Bindings: map[string]WorkerBinding{ "b1": WorkerSecretTextBinding{ Text: "secret text value", }, }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) + }) assert.NoError(t, err) } -func TestWorkers_UploadWorkerWithServiceBinding(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithServiceBinding(t *testing.T) { + setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { @@ -933,407 +828,171 @@ func TestWorkers_UploadWorkerWithServiceBinding(t *testing.T) { assert.Equal(t, expectedBindings, mpUpload.BindingMeta) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, uploadWorkerResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) } - mux.HandleFunc("/accounts/foo/workers/scripts/bar", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - environment := "the_environment" - scriptParams := WorkerScriptParams{ - Script: workerScript, + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, Bindings: map[string]WorkerBinding{ "b1": WorkerServiceBinding{ Service: "the_service", }, "b2": WorkerServiceBinding{ Service: "the_service", - Environment: &environment, + Environment: StringPtr("the_environment"), }, }, - } - _, err := client.UploadWorkerWithBindings(context.Background(), &WorkerRequestParams{ScriptName: "bar"}, &scriptParams) - assert.NoError(t, err) -} - -func TestWorkers_CreateWorkerRoute(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/zones/foo/workers/filters", 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.Fprintf(w, createWorkerRouteResponse) //nolint }) - route := WorkerRoute{Pattern: "app1.example.com/*", Enabled: true} - res, err := client.CreateWorkerRoute(context.Background(), "foo", route) - want := WorkerRouteResponse{successResponse, WorkerRoute{ID: "e7a57d8746e74ae49c25994dadb421b1"}} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} - -func TestWorkers_CreateWorkerRouteEnt(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/zones/foo/workers/routes", 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.Fprintf(w, createWorkerRouteResponse) //nolint - }) - route := WorkerRoute{Pattern: "app1.example.com/*", Script: "test_script"} - res, err := client.CreateWorkerRoute(context.Background(), "foo", route) - want := WorkerRouteResponse{successResponse, WorkerRoute{ID: "e7a57d8746e74ae49c25994dadb421b1"}} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} - -func TestWorkers_CreateWorkerRouteSingleScriptWithAccount(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/zones/foo/workers/filters", 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.Fprintf(w, createWorkerRouteResponse) //nolint - }) - route := WorkerRoute{Pattern: "app1.example.com/*", Enabled: true} - res, err := client.CreateWorkerRoute(context.Background(), "foo", route) - want := WorkerRouteResponse{successResponse, WorkerRoute{ID: "e7a57d8746e74ae49c25994dadb421b1"}} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} - -func TestWorkers_CreateWorkerRouteErrorsWhenMixingSingleAndMultiScriptProperties(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - route := WorkerRoute{Pattern: "app1.example.com/*", Script: "test_script", Enabled: true} - _, err := client.CreateWorkerRoute(context.Background(), "foo", route) - assert.EqualError(t, err, "Only `Script` or `Enabled` may be specified for a WorkerRoute, not both") -} - -func TestWorkers_CreateWorkerRouteWithNoScript(t *testing.T) { - setup(UsingAccount("foo")) - - mux.HandleFunc("/zones/foo/workers/routes", 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.Fprintf(w, createWorkerRouteResponse) //nolint - }) - - route := WorkerRoute{Pattern: "app1.example.com/*"} - _, err := client.CreateWorkerRoute(context.Background(), "foo", route) assert.NoError(t, err) } -func TestWorkers_DeleteWorkerRoute(t *testing.T) { +func TestUploadWorker_WithLogpush(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/routes/e7a57d8746e74ae49c25994dadb421b1", 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.Fprintf(w, deleteWorkerRouteResponseData) //nolint - }) - res, err := client.DeleteWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1") - want := WorkerRouteResponse{successResponse, - WorkerRoute{ - ID: "e7a57d8746e74ae49c25994dadb421b1", - }} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", 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) -func TestWorkers_DeleteWorkerRouteEnt(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() + expected := true + assert.Equal(t, &expected, mpUpload.Logpush) - mux.HandleFunc("/zones/foo/workers/routes/e7a57d8746e74ae49c25994dadb421b1", 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.Fprintf(w, deleteWorkerRouteResponseData) //nolint + fmt.Fprint(w, uploadWorkerResponseData) }) - res, err := client.DeleteWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1") - want := WorkerRouteResponse{successResponse, - WorkerRoute{ - ID: "e7a57d8746e74ae49c25994dadb421b1", + res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerScript, Logpush: BoolPtr(true)}) + formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z") + want := WorkerScriptResponse{ + successResponse, + false, + WorkerScript{ + Script: workerScript, + WorkerMetaData: WorkerMetaData{ + ETAG: "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a", + Size: 191, + ModifiedOn: formattedTime, + }, }} if assert.NoError(t, err) { assert.Equal(t, want, res) } } -func TestWorkers_ListWorkerRoutes(t *testing.T) { +func TestUploadWorker_WithCompatibilityFlags(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/filters", 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.Fprintf(w, listRouteResponseData) //nolint - }) + compatibilityDate := time.Now().Format("2006-01-02") + compatibilityFlags := []string{"formdata_parser_supports_files"} - res, err := client.ListWorkerRoutes(context.Background(), "foo") - want := WorkerRoutesResponse{successResponse, - []WorkerRoute{ - {ID: "e7a57d8746e74ae49c25994dadb421b1", Pattern: "app1.example.com/*", Enabled: true}, - {ID: "f8b68e9857f85bf59c25994dadb421b1", Pattern: "app2.example.com/*", Enabled: false}, - }, - } - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) -func TestWorkers_ListWorkerRoutesEnt(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() + mpUpload, err := parseMultipartUpload(r) + assert.NoError(t, err) - mux.HandleFunc("/zones/foo/workers/routes", 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.Fprintf(w, listRouteEntResponseData) //nolint - }) + assert.Equal(t, workerScript, mpUpload.Script) + assert.Equal(t, compatibilityDate, mpUpload.CompatibilityDate) + assert.Equal(t, compatibilityFlags, mpUpload.CompatibilityFlags) - res, err := client.ListWorkerRoutes(context.Background(), "foo") - want := WorkerRoutesResponse{successResponse, - []WorkerRoute{ - {ID: "e7a57d8746e74ae49c25994dadb421b1", Pattern: "app1.example.com/*", Script: "test_script_1", Enabled: true}, - {ID: "f8b68e9857f85bf59c25994dadb421b1", Pattern: "app2.example.com/*", Script: "test_script_2", Enabled: true}, - {ID: "2b5bf4240cd34c77852fac70b1bf745a", Pattern: "app3.example.com/*", Script: "", Enabled: false}, - }, - } - if assert.NoError(t, err) { - assert.Equal(t, want, res) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, uploadWorkerResponseData) } -} - -func TestWorkers_GetWorkerRoute(t *testing.T) { - setup() - defer teardown() + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - mux.HandleFunc("/zones/foo/workers/routes/1234", 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.Fprintf(w, getRouteResponseData) //nolint + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, + CompatibilityDate: compatibilityDate, + CompatibilityFlags: compatibilityFlags, }) - - res, err := client.GetWorkerRoute(context.Background(), "foo", "1234") - want := WorkerRouteResponse{successResponse, - WorkerRoute{ - ID: "e7a57d8746e74ae49c25994dadb421b1", - Pattern: "app1.example.com/*", - Script: "script-name"}, - } - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } + assert.NoError(t, err) } -func TestWorkers_UpdateWorkerRoute(t *testing.T) { +func TestUploadWorker_WithQueueBinding(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/zones/foo/workers/filters/e7a57d8746e74ae49c25994dadb421b1", func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) - w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, updateWorkerRouteResponse) //nolint - }) - route := WorkerRoute{Pattern: "app3.example.com/*", Enabled: true} - res, err := client.UpdateWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1", route) - want := WorkerRouteResponse{successResponse, - WorkerRoute{ - ID: "e7a57d8746e74ae49c25994dadb421b1", - Pattern: "app3.example.com/*", - Enabled: true, - }} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} -func TestWorkers_UpdateWorkerRouteEnt(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/zones/foo/workers/routes/e7a57d8746e74ae49c25994dadb421b1", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) - w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, updateWorkerRouteEntResponse) //nolint - }) - route := WorkerRoute{Pattern: "app3.example.com/*", Script: "test_script_1"} - res, err := client.UpdateWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1", route) - want := WorkerRouteResponse{successResponse, - WorkerRoute{ - ID: "e7a57d8746e74ae49c25994dadb421b1", - Pattern: "app3.example.com/*", - Script: "test_script_1", - }} - if assert.NoError(t, err) { - assert.Equal(t, want, res) - } -} + mpUpload, err := parseMultipartUpload(r) + assert.NoError(t, err) -func TestWorkers_UpdateWorkerRouteSingleScriptWithAccount(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() + expectedBindings := map[string]workerBindingMeta{ + "b1": { + "name": "b1", + "type": "queue", + "queue_name": "test-queue", + }, + } + assert.Equal(t, workerScript, mpUpload.Script) + assert.Equal(t, expectedBindings, mpUpload.BindingMeta) - mux.HandleFunc("/zones/foo/workers/filters/e7a57d8746e74ae49c25994dadb421b1", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, updateWorkerRouteEntResponse) //nolint - }) - route := WorkerRoute{Pattern: "app3.example.com/*", Enabled: true} - res, err := client.UpdateWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1", route) - want := WorkerRouteResponse{successResponse, - WorkerRoute{ - ID: "e7a57d8746e74ae49c25994dadb421b1", - Pattern: "app3.example.com/*", - Script: "test_script_1", - }} - if assert.NoError(t, err) { - assert.Equal(t, want, res) + fmt.Fprint(w, uploadWorkerResponseData) } -} - -func TestWorkers_ListWorkerBindingsMultiScript(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) - mux.HandleFunc("/accounts/foo/workers/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.Fprintf(w, listBindingsResponseData) //nolint - }) - - mux.HandleFunc("/accounts/foo/workers/scripts/my-script/bindings/MY_WASM/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", "application/wasm") - _, _ = w.Write([]byte("mock multi-script wasm")) - }) - - res, err := client.ListWorkerBindings(context.Background(), &WorkerRequestParams{ - ScriptName: "my-script", - }) - assert.NoError(t, err) - - assert.Equal(t, successResponse, res.Response) - assert.Equal(t, 8, 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()) - - assert.Equal(t, "MY_WASM", res.BindingList[1].Name) - wasmBinding := res.BindingList[1].Binding.(WorkerWebAssemblyBinding) - wasmModuleContent, err := io.ReadAll(wasmBinding.Module) + _, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, + Bindings: map[string]WorkerBinding{ + "b1": WorkerQueueBinding{ + Binding: "b1", + Queue: "test-queue", + }, + }}) assert.NoError(t, err) - assert.Equal(t, []byte("mock multi-script wasm"), wasmModuleContent) - assert.Equal(t, WorkerWebAssemblyBindingType, res.BindingList[1].Binding.Type()) - - 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()) } -func TestWorkers_UpdateWorkerRouteErrorsWhenMixingSingleAndMultiScriptProperties(t *testing.T) { - setup(UsingAccount("foo")) +func TestUploadWorker_WithSmartPlacementEnabled(t *testing.T) { + setup() defer teardown() - route := WorkerRoute{Pattern: "app1.example.com/*", Script: "test_script", Enabled: true} - _, err := client.UpdateWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1", route) - assert.EqualError(t, err, "Only `Script` or `Enabled` may be specified for a WorkerRoute, not both") -} + placementMode := PlacementModeSmart + response := uploadModuleWorkerSmartPlacement -func TestWorkers_UpdateWorkerRouteWithNoScript(t *testing.T) { - setup(UsingAccount("foo")) - defer teardown() - - mux.HandleFunc("/zones/foo/workers/routes/e7a57d8746e74ae49c25994dadb421b1", func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) - w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, updateWorkerRouteEntResponse) //nolint - }) - route := WorkerRoute{Pattern: "app1.example.com/*"} - _, err := client.UpdateWorkerRoute(context.Background(), "foo", "e7a57d8746e74ae49c25994dadb421b1", route) - assert.NoError(t, err) -} + mpUpload, err := parseMultipartUpload(r) + assert.NoError(t, err) -func TestWorkers_AttachWorkerToDomain(t *testing.T) { - setup(UsingAccount(testAccountID)) - defer teardown() + assert.Equal(t, workerScript, mpUpload.Script) - mux.HandleFunc("/accounts/"+testAccountID+"/workers/domains", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/json") - fmt.Fprintf(w, attachWorkerToDomainResponse) //nolint + fmt.Fprint(w, response) + } + mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler) + + t.Run("Test enabling Smart Placement", func(t *testing.T) { + worker, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, + Placement: &Placement{ + Mode: placementMode, + }, + }) + assert.NoError(t, err) + assert.Equal(t, placementMode, *worker.PlacementMode) }) - res, err := client.AttachWorkerToDomain(context.Background(), AccountIdentifier(testAccountID), &WorkerDomainParams{ - ZoneID: testZoneID, - Hostname: "app4.example.com", - Service: "test_script_1", - Environment: "production", + + t.Run("Test disabling placement", func(t *testing.T) { + placementMode = PlacementModeOff + response = uploadWorkerModuleResponseData + + worker, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ + ScriptName: "bar", + Script: workerScript, + Placement: &Placement{ + Mode: placementMode, + }, + }) + assert.NoError(t, err) + assert.Nil(t, worker.PlacementMode) }) - want := WorkerDomainResponse{ - successResponse, - WorkerDomainResult{ - ID: "e7a57d8746e74ae49c25994dadb421b1", - ZoneID: testZoneID, - Service: "test_script_1", - Hostname: "api4.example.com", - Environment: "production", - }} - if assert.NoError(t, err) { - assert.Equal(t, want.Response, res.Response) - } } diff --git a/zone.go b/zone.go index 3929356000..59489f50c9 100644 --- a/zone.go +++ b/zone.go @@ -14,6 +14,11 @@ import ( "golang.org/x/net/idna" ) +var ( + // ErrMissingSettingName is for when setting name is required but missing. + ErrMissingSettingName = errors.New("zone setting name required but missing") +) + // Owner describes the resource owner. type Owner struct { ID string `json:"id"` @@ -300,6 +305,17 @@ type zoneSubscriptionRatePlanPayload struct { } `json:"rate_plan"` } +type GetZoneSettingParams struct { + Name string `json:"-"` + PathPrefix string `json:"-"` +} + +type UpdateZoneSettingParams struct { + Name string `json:"-"` + PathPrefix string `json:"-"` + Value interface{} `json:"value"` +} + // CreateZone creates a zone on an account. // // Setting jumpstart to true will attempt to automatically scan for existing @@ -722,10 +738,10 @@ func (api *API) AvailableZonePlans(ctx context.Context, zoneID string) ([]ZonePl func (o ZoneAnalyticsOptions) encode() string { v := url.Values{} if o.Since != nil { - v.Set("since", (*o.Since).Format(time.RFC3339)) + v.Set("since", o.Since.Format(time.RFC3339)) } if o.Until != nil { - v.Set("until", (*o.Until).Format(time.RFC3339)) + v.Set("until", o.Until.Format(time.RFC3339)) } if o.Continuous != nil { v.Set("continuous", fmt.Sprintf("%t", *o.Continuous)) @@ -897,11 +913,25 @@ func normalizeZoneName(name string) string { return name } -// ZoneSingleSetting returns information about specified setting to the specified zone. +// GetZoneSetting returns information about specified setting to the specified +// zone. // // API reference: https://api.cloudflare.com/#zone-settings-get-all-zone-settings -func (api *API) ZoneSingleSetting(ctx context.Context, zoneID, settingName string) (ZoneSetting, error) { - uri := fmt.Sprintf("/zones/%s/settings/%s", zoneID, settingName) +func (api *API) GetZoneSetting(ctx context.Context, rc *ResourceContainer, params GetZoneSettingParams) (ZoneSetting, error) { + if rc.Level != ZoneRouteLevel { + return ZoneSetting{}, ErrRequiredZoneLevelResourceContainer + } + + if rc.Identifier == "" { + return ZoneSetting{}, ErrMissingName + } + + pathPrefix := "settings" + if params.PathPrefix != "" { + pathPrefix = params.PathPrefix + } + + uri := fmt.Sprintf("/zones/%s/%s/%s", rc.Identifier, pathPrefix, params.Name) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return ZoneSetting{}, err @@ -914,23 +944,36 @@ func (api *API) ZoneSingleSetting(ctx context.Context, zoneID, settingName strin return r.Result, nil } -// UpdateZoneSingleSetting updates the specified setting for a given zone. +// UpdateZoneSetting updates the specified setting for a given zone. // // API reference: https://api.cloudflare.com/#zone-settings-edit-zone-settings-info -func (api *API) UpdateZoneSingleSetting(ctx context.Context, zoneID, settingName string, setting ZoneSetting) (*ZoneSettingSingleResponse, error) { - uri := fmt.Sprintf("/zones/%s/settings/%s", zoneID, settingName) - res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, setting) +func (api *API) UpdateZoneSetting(ctx context.Context, rc *ResourceContainer, params UpdateZoneSettingParams) (ZoneSetting, error) { + if rc.Level != ZoneRouteLevel { + return ZoneSetting{}, ErrRequiredZoneLevelResourceContainer + } + + if rc.Identifier == "" { + return ZoneSetting{}, ErrMissingName + } + + pathPrefix := "settings" + if params.PathPrefix != "" { + pathPrefix = params.PathPrefix + } + + uri := fmt.Sprintf("/zones/%s/%s/%s", rc.Identifier, pathPrefix, params.Name) + res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params) if err != nil { - return nil, err + return ZoneSetting{}, err } response := &ZoneSettingSingleResponse{} err = json.Unmarshal(res, &response) if err != nil { - return nil, fmt.Errorf("%s: %w", errUnmarshalError, err) + return ZoneSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err) } - return response, nil + return response.Result, nil } // ZoneExport returns the text BIND config for the given zone diff --git a/zone_test.go b/zone_test.go index 93a2bb6fd5..6dd5bc4fcc 100644 --- a/zone_test.go +++ b/zone_test.go @@ -1510,3 +1510,103 @@ func TestUpdateZoneSSLSettings(t *testing.T) { assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") } } + +func TestGetZoneSetting(t *testing.T) { + setup() + defer teardown() + handler := 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.Fprintf(w, `{ + "result": { + "id": "ssl", + "value": "off", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z" + } + }`) + } + mux.HandleFunc("/zones/foo/settings/ssl", handler) + s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl"}) + if assert.NoError(t, err) { + assert.Equal(t, s.ID, "ssl") + assert.Equal(t, s.Value, "off") + assert.Equal(t, s.Editable, true) + assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") + } +} + +func TestGetZoneSettingWithCustomPathPrefix(t *testing.T) { + setup() + defer teardown() + handler := 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.Fprintf(w, `{ + "result": { + "id": "ssl", + "value": "off", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z" + } + }`) + } + mux.HandleFunc("/zones/foo/my_custom_path/ssl", handler) + s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl", PathPrefix: "my_custom_path"}) + if assert.NoError(t, err) { + assert.Equal(t, s.ID, "ssl") + assert.Equal(t, s.Value, "off") + assert.Equal(t, s.Editable, true) + assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") + } +} + +func TestUpdateZoneSetting(t *testing.T) { + setup() + defer teardown() + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + _, _ = fmt.Fprintf(w, `{ + "result": { + "id": "ssl", + "value": "off", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z" + } + }`) + } + mux.HandleFunc("/zones/foo/settings/ssl", handler) + s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{Name: "ssl", Value: "off"}) + if assert.NoError(t, err) { + assert.Equal(t, s.ID, "ssl") + assert.Equal(t, s.Value, "off") + assert.Equal(t, s.Editable, true) + assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") + } +} + +func TestUpdateZoneSettingWithCustomPathPrefix(t *testing.T) { + setup() + defer teardown() + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) + w.Header().Set("content-type", "application/json") + _, _ = fmt.Fprintf(w, `{ + "result": { + "id": "ssl", + "value": "off", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z" + } + }`) + } + mux.HandleFunc("/zones/foo/my_custom_path/ssl", handler) + s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{Name: "ssl", PathPrefix: "my_custom_path", Value: "off"}) + if assert.NoError(t, err) { + assert.Equal(t, s.ID, "ssl") + assert.Equal(t, s.Value, "off") + assert.Equal(t, s.Editable, true) + assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") + } +}