diff --git a/.changelog/1264.txt b/.changelog/1264.txt index 55ed0957708..65b0d20e22d 100644 --- a/.changelog/1264.txt +++ b/.changelog/1264.txt @@ -1,19 +1,19 @@ -```release-note:breaking-change -pages_deployment: add support for auto pagination -``` - -```release-note:enchancement -pages_deployment: add Force to DeletePagesDeploymentParams -``` - -```release-note:breaking-change -pages_deployment: change DeletePagesDeploymentParams to contain all parameters -``` - -```release-note:breaking-change -pages_project: rename PagesProject to GetPagesProject -``` - -```release-note:breaking-change -pages_project: change to use ResourceContainer for account ID -``` \ No newline at end of file +```release-note:breaking-change +pages_deployment: add support for auto pagination +``` + +```release-note:enchancement +pages_deployment: add Force to DeletePagesDeploymentParams +``` + +```release-note:breaking-change +pages_deployment: change DeletePagesDeploymentParams to contain all parameters +``` + +```release-note:breaking-change +pages_project: rename PagesProject to GetPagesProject +``` + +```release-note:breaking-change +pages_project: change to use ResourceContainer for account ID +``` diff --git a/.changelog/1265.txt b/.changelog/1265.txt index f93fc420a6a..a001f26c5db 100644 --- a/.changelog/1265.txt +++ b/.changelog/1265.txt @@ -1,7 +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 -``` +```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/1319.txt b/.changelog/1319.txt index be082a58648..4235c885cf0 100644 --- a/.changelog/1319.txt +++ b/.changelog/1319.txt @@ -1,59 +1,59 @@ -```release-note:breaking-change -access_application: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:enhancement -access_application: add support for auto pagination -``` - -```release-note:breaking-change -access_ca_certificate: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:enhancement -access_ca_certificate: add support for auto pagination -``` - -```release-note:breaking-change -access_group: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:enhancement -access_group: add support for auto pagination -``` - -```release-note:breaking-change -access_identity_provider: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:enhancement -access_identity_provider: add support for auto pagination -``` - -```release-note:breaking-change -access_mutual_tls_certificates: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:enhancement -access_mutual_tls_certificates: add support for auto pagination -``` - -```release-note:breaking-change -access_organization: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:breaking-change -access_policy: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:enhancement -access_policy: add support for auto pagination -``` - -```release-note:breaking-change -access_service_tokens: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` - -```release-note:breaking-change -access_user_token: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods -``` +```release-note:breaking-change +access_application: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:enhancement +access_application: add support for auto pagination +``` + +```release-note:breaking-change +access_ca_certificate: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:enhancement +access_ca_certificate: add support for auto pagination +``` + +```release-note:breaking-change +access_group: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:enhancement +access_group: add support for auto pagination +``` + +```release-note:breaking-change +access_identity_provider: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:enhancement +access_identity_provider: add support for auto pagination +``` + +```release-note:breaking-change +access_mutual_tls_certificates: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:enhancement +access_mutual_tls_certificates: add support for auto pagination +``` + +```release-note:breaking-change +access_organization: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:breaking-change +access_policy: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:enhancement +access_policy: add support for auto pagination +``` + +```release-note:breaking-change +access_service_tokens: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` + +```release-note:breaking-change +access_user_token: refactor methods to use `ResourceContainer` instead of dedicated account/zone methods +``` diff --git a/.changelog/2793.txt b/.changelog/2793.txt new file mode 100644 index 00000000000..af48c245270 --- /dev/null +++ b/.changelog/2793.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +access_application: add `skip_app_launcher_login_page` flag to skip the App Launcher landing page +``` diff --git a/.changelog/2812.txt b/.changelog/2812.txt new file mode 100644 index 00000000000..f9d178c0ed5 --- /dev/null +++ b/.changelog/2812.txt @@ -0,0 +1,3 @@ +```release-notes:enhancement +worker_bindings: add support for `hyperdrive` bindings +``` diff --git a/.changelog/2816.txt b/.changelog/2816.txt new file mode 100644 index 00000000000..9d9a5676be9 --- /dev/null +++ b/.changelog/2816.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/time from 0.5.0 to 0.6.0 +``` diff --git a/.changelog/2833.txt b/.changelog/2833.txt new file mode 100644 index 00000000000..00633a04e66 --- /dev/null +++ b/.changelog/2833.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +rulesets: Add `DeleteRulesetRule` +``` diff --git a/.changelog/2835.txt b/.changelog/2835.txt new file mode 100644 index 00000000000..1071fa6bcbf --- /dev/null +++ b/.changelog/2835.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.27.0 to 0.28.0 +``` diff --git a/.changelog/2838.txt b/.changelog/2838.txt new file mode 100644 index 00000000000..4a9c03ec0e6 --- /dev/null +++ b/.changelog/2838.txt @@ -0,0 +1,3 @@ +```release-notes:bug +access_application: enable removing access application custom attributes and claims +``` diff --git a/.changelog/2855.txt b/.changelog/2855.txt new file mode 100644 index 00000000000..614807de5c4 --- /dev/null +++ b/.changelog/2855.txt @@ -0,0 +1,3 @@ +```release-notes:enhancement +ssl: add support for ssl/tls automatic mode setting +``` diff --git a/.changelog/2857.txt b/.changelog/2857.txt new file mode 100644 index 00000000000..f27aee94771 --- /dev/null +++ b/.changelog/2857.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +errors: implement the Unwrap method for custom error types to access the wrapped errors via errors.Is and errors.As +``` diff --git a/.changelog/2863.txt b/.changelog/2863.txt new file mode 100644 index 00000000000..93795789d3b --- /dev/null +++ b/.changelog/2863.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps github.com/urfave/cli/v2 from 2.27.3 to 2.27.4 +``` diff --git a/.changelog/2886.txt b/.changelog/2886.txt new file mode 100644 index 00000000000..d87d819640f --- /dev/null +++ b/.changelog/2886.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +rulesets: Export `DeleteRulesetRuleParams` fields +``` diff --git a/.changelog/2887.txt b/.changelog/2887.txt new file mode 100644 index 00000000000..5015cdb00c8 --- /dev/null +++ b/.changelog/2887.txt @@ -0,0 +1,3 @@ +```release-notes:bug +cosmetic: Fix CRLF in changelog and test fixtures +``` diff --git a/.changelog/2931.txt b/.changelog/2931.txt new file mode 100644 index 00000000000..775a2085f47 --- /dev/null +++ b/.changelog/2931.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +waiting_room: add support for `enabled_origin_commands` +``` \ No newline at end of file diff --git a/.changelog/2932.txt b/.changelog/2932.txt new file mode 100644 index 00000000000..d9b098e61b3 --- /dev/null +++ b/.changelog/2932.txt @@ -0,0 +1,3 @@ +```release-notes:enhancement +worker_bindings: Adjust struct of `hyperdrive` bindings to better align with wrangler.toml syntax +``` diff --git a/.changelog/2935.txt b/.changelog/2935.txt new file mode 100644 index 00000000000..bcf3ae09457 --- /dev/null +++ b/.changelog/2935.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +rulesets: add "contains" field to custom cache key header +``` diff --git a/.changelog/2937.txt b/.changelog/2937.txt new file mode 100644 index 00000000000..a2c91801be9 --- /dev/null +++ b/.changelog/2937.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +teams_certificates: renamed `enabled` to `in_use` +``` diff --git a/.changelog/2974.txt b/.changelog/2974.txt new file mode 100644 index 00000000000..ff0f072ef6a --- /dev/null +++ b/.changelog/2974.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +bot_management: add ai_bots_protection to public API +``` diff --git a/.changelog/2986.txt b/.changelog/2986.txt new file mode 100644 index 00000000000..a717be7508f --- /dev/null +++ b/.changelog/2986.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +dns: removed deprecated `ZoneID` and `ZoneName` fields +``` diff --git a/.changelog/3027.txt b/.changelog/3027.txt new file mode 100644 index 00000000000..a6d75bb371a --- /dev/null +++ b/.changelog/3027.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +load_balancing: add account load balancer support +``` \ No newline at end of file diff --git a/.changelog/3030.txt b/.changelog/3030.txt new file mode 100644 index 00000000000..1d25b5739ab --- /dev/null +++ b/.changelog/3030.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.28.0 to 0.29.0 +``` diff --git a/.changelog/3031.txt b/.changelog/3031.txt new file mode 100644 index 00000000000..aa5e89ebb08 --- /dev/null +++ b/.changelog/3031.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +device_posture_rule: add score field for custom_s2s posture rule +``` \ No newline at end of file diff --git a/.changelog/3184.txt b/.changelog/3184.txt new file mode 100644 index 00000000000..e0c858c3517 --- /dev/null +++ b/.changelog/3184.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +infrastructure_targets: initialize CRUD endpoints for infrastructure access endpoints +``` diff --git a/.changelog/3186.txt b/.changelog/3186.txt new file mode 100644 index 00000000000..c0d9da1db8b --- /dev/null +++ b/.changelog/3186.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +access_application: added target contexts support for access application type infrastructure +``` + +```release-note:enhancement +access_policy: added infrastructure connection rule support for access policy +``` diff --git a/.changelog/3321.txt b/.changelog/3321.txt new file mode 100644 index 00000000000..a5aa56db05a --- /dev/null +++ b/.changelog/3321.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/time from 0.6.0 to 0.7.0 +``` diff --git a/.changelog/3336.txt b/.changelog/3336.txt new file mode 100644 index 00000000000..c5e48f1f166 --- /dev/null +++ b/.changelog/3336.txt @@ -0,0 +1,3 @@ +```release-note:dependency +deps: bumps golang.org/x/net from 0.29.0 to 0.30.0 +``` diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000000..4090692fe0b --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,24 @@ +on: + pull_request: {} + workflow_dispatch: {} + push: + branches: + - main + - master + schedule: + - cron: '0 0 * * *' +name: Semgrep config +jobs: + semgrep: + name: semgrep/ci + runs-on: ubuntu-latest + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + SEMGREP_URL: https://cloudflare.semgrep.dev + SEMGREP_APP_URL: https://cloudflare.semgrep.dev + SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version + container: + image: semgrep/semgrep + steps: + - uses: actions/checkout@v4 + - run: semgrep ci diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b8fa6513e..b7b23bd0a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,72 @@ -## 0.101.0 (Unreleased) +## 0.107.0 (Unreleased) + +## 0.106.0 (September 27th, 2024) + +ENHANCEMENTS: + +* access_application: added target contexts support for access application type infrastructure ([#3186](https://github.com/cloudflare/cloudflare-go/issues/3186)) +* access_policy: added infrastructure connection rule support for access policy ([#3186](https://github.com/cloudflare/cloudflare-go/issues/3186)) + +## 0.105.0 (September 25th, 2024) + +ENHANCEMENTS: + +* device_posture_rule: add score field for custom_s2s posture rule ([#3031](https://github.com/cloudflare/cloudflare-go/issues/3031)) +* infrastructure_targets: initialize CRUD endpoints for infrastructure access endpoints ([#3184](https://github.com/cloudflare/cloudflare-go/issues/3184)) +* load_balancing: add account load balancer support ([#3027](https://github.com/cloudflare/cloudflare-go/issues/3027)) + +## 0.104.0 (September 11th, 2024) + +BREAKING CHANGES: + +* dns: removed deprecated `ZoneID` and `ZoneName` fields ([#2986](https://github.com/cloudflare/cloudflare-go/issues/2986)) + +ENHANCEMENTS: + +* bot_management: add ai_bots_protection to public API ([#2974](https://github.com/cloudflare/cloudflare-go/issues/2974)) + +DEPENDENCIES: + +* deps: bumps golang.org/x/net from 0.28.0 to 0.29.0 ([#3030](https://github.com/cloudflare/cloudflare-go/issues/3030)) +* deps: bumps golang.org/x/time from 0.5.0 to 0.6.0 ([#2816](https://github.com/cloudflare/cloudflare-go/issues/2816)) + +## 0.103.0 (August 28th, 2024) + +ENHANCEMENTS: + +* errors: implement the Unwrap method for custom error types to access the wrapped errors via errors.Is and errors.As ([#2857](https://github.com/cloudflare/cloudflare-go/issues/2857)) +* rulesets: add "contains" field to custom cache key header ([#2935](https://github.com/cloudflare/cloudflare-go/issues/2935)) +* teams_certificates: renamed `enabled` to `in_use` ([#2937](https://github.com/cloudflare/cloudflare-go/issues/2937)) +* waiting_room: add support for `enabled_origin_commands` ([#2931](https://github.com/cloudflare/cloudflare-go/issues/2931)) + +DEPENDENCIES: + +* deps: bumps github.com/urfave/cli/v2 from 2.27.3 to 2.27.4 ([#2863](https://github.com/cloudflare/cloudflare-go/issues/2863)) + +## 0.102.0 (August 14th, 2024) + +ENHANCEMENTS: + +* rulesets: Add `DeleteRulesetRule` ([#2833](https://github.com/cloudflare/cloudflare-go/issues/2833)) +* rulesets: Export `DeleteRulesetRuleParams` fields ([#2886](https://github.com/cloudflare/cloudflare-go/issues/2886)) +* teams_accounts: Add `disable_for_time` attribute ([#2797](https://github.com/cloudflare/cloudflare-go/issues/2797)) + +DEPENDENCIES: + +* deps: bumps golang.org/x/net from 0.27.0 to 0.28.0 ([#2835](https://github.com/cloudflare/cloudflare-go/issues/2835)) + +## 0.101.0 (July 31st, 2024) + +ENHANCEMENTS: + +* access_application: add `skip_app_launcher_login_page` flag to skip the App Launcher landing page ([#2793](https://github.com/cloudflare/cloudflare-go/issues/2793)) +* device_posture_rule: support extended_key_usage, check_private_key, and locations for client_certificate_v2 posture rule ([#1685](https://github.com/cloudflare/cloudflare-go/issues/1685)) +* devices_policy: Add new tunnel_protocol field to policy ([#2778](https://github.com/cloudflare/cloudflare-go/issues/2778)) +* risk_score_integration: Add support for Risk Score Integrations ([#2786](https://github.com/cloudflare/cloudflare-go/issues/2786)) + +DEPENDENCIES: + +* deps: bumps github.com/urfave/cli/v2 from 2.27.2 to 2.27.3 ([#2787](https://github.com/cloudflare/cloudflare-go/issues/2787)) ## 0.100.0 (July 18th, 2024) diff --git a/access_application.go b/access_application.go index bdeaf79cd8d..2944cf114da 100644 --- a/access_application.go +++ b/access_application.go @@ -15,50 +15,52 @@ type AccessApplicationType string // These constants represent all valid application types. const ( - SelfHosted AccessApplicationType = "self_hosted" - SSH AccessApplicationType = "ssh" - VNC AccessApplicationType = "vnc" - Biso AccessApplicationType = "biso" - AppLauncher AccessApplicationType = "app_launcher" - Warp AccessApplicationType = "warp" - Bookmark AccessApplicationType = "bookmark" - Saas AccessApplicationType = "saas" + SelfHosted AccessApplicationType = "self_hosted" + SSH AccessApplicationType = "ssh" + VNC AccessApplicationType = "vnc" + Biso AccessApplicationType = "biso" + AppLauncher AccessApplicationType = "app_launcher" + Warp AccessApplicationType = "warp" + Bookmark AccessApplicationType = "bookmark" + Saas AccessApplicationType = "saas" + Infrastructure AccessApplicationType = "infrastructure" ) // AccessApplication represents an Access application. type AccessApplication struct { - GatewayRules []AccessApplicationGatewayRule `json:"gateway_rules,omitempty"` - AllowedIdps []string `json:"allowed_idps,omitempty"` - CustomDenyMessage string `json:"custom_deny_message,omitempty"` - LogoURL string `json:"logo_url,omitempty"` - AUD string `json:"aud,omitempty"` - Domain string `json:"domain"` - SelfHostedDomains []string `json:"self_hosted_domains"` - Type AccessApplicationType `json:"type,omitempty"` - SessionDuration string `json:"session_duration,omitempty"` - SameSiteCookieAttribute string `json:"same_site_cookie_attribute,omitempty"` - CustomDenyURL string `json:"custom_deny_url,omitempty"` - CustomNonIdentityDenyURL string `json:"custom_non_identity_deny_url,omitempty"` - Name string `json:"name"` - ID string `json:"id,omitempty"` - PrivateAddress string `json:"private_address"` - CorsHeaders *AccessApplicationCorsHeaders `json:"cors_headers,omitempty"` - CreatedAt *time.Time `json:"created_at,omitempty"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` - SaasApplication *SaasApplication `json:"saas_app,omitempty"` - AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` - SkipInterstitial *bool `json:"skip_interstitial,omitempty"` - AppLauncherVisible *bool `json:"app_launcher_visible,omitempty"` - 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"` - AllowAuthenticateViaWarp *bool `json:"allow_authenticate_via_warp,omitempty"` - OptionsPreflightBypass *bool `json:"options_preflight_bypass,omitempty"` - CustomPages []string `json:"custom_pages,omitempty"` - Tags []string `json:"tags,omitempty"` - SCIMConfig *AccessApplicationSCIMConfig `json:"scim_config,omitempty"` - Policies []AccessPolicy `json:"policies,omitempty"` + GatewayRules []AccessApplicationGatewayRule `json:"gateway_rules,omitempty"` + AllowedIdps []string `json:"allowed_idps,omitempty"` + CustomDenyMessage string `json:"custom_deny_message,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + AUD string `json:"aud,omitempty"` + Domain string `json:"domain"` + SelfHostedDomains []string `json:"self_hosted_domains"` + Type AccessApplicationType `json:"type,omitempty"` + SessionDuration string `json:"session_duration,omitempty"` + SameSiteCookieAttribute string `json:"same_site_cookie_attribute,omitempty"` + CustomDenyURL string `json:"custom_deny_url,omitempty"` + CustomNonIdentityDenyURL string `json:"custom_non_identity_deny_url,omitempty"` + Name string `json:"name"` + ID string `json:"id,omitempty"` + PrivateAddress string `json:"private_address"` + CorsHeaders *AccessApplicationCorsHeaders `json:"cors_headers,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + SaasApplication *SaasApplication `json:"saas_app,omitempty"` + AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` + SkipInterstitial *bool `json:"skip_interstitial,omitempty"` + AppLauncherVisible *bool `json:"app_launcher_visible,omitempty"` + 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"` + AllowAuthenticateViaWarp *bool `json:"allow_authenticate_via_warp,omitempty"` + OptionsPreflightBypass *bool `json:"options_preflight_bypass,omitempty"` + CustomPages []string `json:"custom_pages,omitempty"` + Tags []string `json:"tags,omitempty"` + SCIMConfig *AccessApplicationSCIMConfig `json:"scim_config,omitempty"` + Policies []AccessPolicy `json:"policies,omitempty"` + TargetContexts *[]AccessInfrastructureTargetContext `json:"target_criteria,omitempty"` AccessAppLauncherCustomization } @@ -224,15 +226,15 @@ type SaasApplication struct { AuthType string `json:"auth_type,omitempty"` // SAML saas app - ConsumerServiceUrl string `json:"consumer_service_url,omitempty"` - SPEntityID string `json:"sp_entity_id,omitempty"` - IDPEntityID string `json:"idp_entity_id,omitempty"` - NameIDFormat string `json:"name_id_format,omitempty"` - SSOEndpoint string `json:"sso_endpoint,omitempty"` - DefaultRelayState string `json:"default_relay_state,omitempty"` - CustomAttributes []SAMLAttributeConfig `json:"custom_attributes,omitempty"` - NameIDTransformJsonata string `json:"name_id_transform_jsonata,omitempty"` - SamlAttributeTransformJsonata string `json:"saml_attribute_transform_jsonata"` + ConsumerServiceUrl string `json:"consumer_service_url,omitempty"` + SPEntityID string `json:"sp_entity_id,omitempty"` + IDPEntityID string `json:"idp_entity_id,omitempty"` + NameIDFormat string `json:"name_id_format,omitempty"` + SSOEndpoint string `json:"sso_endpoint,omitempty"` + DefaultRelayState string `json:"default_relay_state,omitempty"` + CustomAttributes *[]SAMLAttributeConfig `json:"custom_attributes"` + NameIDTransformJsonata string `json:"name_id_transform_jsonata,omitempty"` + SamlAttributeTransformJsonata string `json:"saml_attribute_transform_jsonata"` // OIDC saas app ClientID string `json:"client_id,omitempty"` @@ -242,7 +244,7 @@ type SaasApplication struct { Scopes []string `json:"scopes,omitempty"` AppLauncherURL string `json:"app_launcher_url,omitempty"` GroupFilterRegex string `json:"group_filter_regex,omitempty"` - CustomClaims []OIDCClaimConfig `json:"custom_claims,omitempty"` + CustomClaims *[]OIDCClaimConfig `json:"custom_claims"` AllowPKCEWithoutClientSecret *bool `json:"allow_pkce_without_client_secret,omitempty"` RefreshTokenOptions *RefreshTokenOptions `json:"refresh_token_options,omitempty"` HybridAndImplicitOptions *AccessApplicationHybridAndImplicitOptions `json:"hybrid_and_implicit_options,omitempty"` @@ -250,11 +252,12 @@ type SaasApplication struct { } type AccessAppLauncherCustomization struct { - LandingPageDesign AccessLandingPageDesign `json:"landing_page_design"` - LogoURL string `json:"app_launcher_logo_url"` - HeaderBackgroundColor string `json:"header_bg_color"` - BackgroundColor string `json:"bg_color"` - FooterLinks []AccessFooterLink `json:"footer_links"` + LandingPageDesign AccessLandingPageDesign `json:"landing_page_design"` + LogoURL string `json:"app_launcher_logo_url"` + HeaderBackgroundColor string `json:"header_bg_color"` + BackgroundColor string `json:"bg_color"` + FooterLinks []AccessFooterLink `json:"footer_links"` + SkipAppLauncherLoginPage *bool `json:"skip_app_launcher_login_page,omitempty"` } type AccessFooterLink struct { @@ -275,69 +278,71 @@ type ListAccessApplicationsParams struct { } type CreateAccessApplicationParams struct { - AllowedIdps []string `json:"allowed_idps,omitempty"` - AppLauncherVisible *bool `json:"app_launcher_visible,omitempty"` - AUD string `json:"aud,omitempty"` - AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` - CorsHeaders *AccessApplicationCorsHeaders `json:"cors_headers,omitempty"` - CustomDenyMessage string `json:"custom_deny_message,omitempty"` - CustomDenyURL string `json:"custom_deny_url,omitempty"` - CustomNonIdentityDenyURL string `json:"custom_non_identity_deny_url,omitempty"` - Domain string `json:"domain"` - EnableBindingCookie *bool `json:"enable_binding_cookie,omitempty"` - GatewayRules []AccessApplicationGatewayRule `json:"gateway_rules,omitempty"` - HttpOnlyCookieAttribute *bool `json:"http_only_cookie_attribute,omitempty"` - LogoURL string `json:"logo_url,omitempty"` - Name string `json:"name"` - PathCookieAttribute *bool `json:"path_cookie_attribute,omitempty"` - PrivateAddress string `json:"private_address"` - SaasApplication *SaasApplication `json:"saas_app,omitempty"` - SameSiteCookieAttribute string `json:"same_site_cookie_attribute,omitempty"` - SelfHostedDomains []string `json:"self_hosted_domains"` - ServiceAuth401Redirect *bool `json:"service_auth_401_redirect,omitempty"` - SessionDuration string `json:"session_duration,omitempty"` - SkipInterstitial *bool `json:"skip_interstitial,omitempty"` - OptionsPreflightBypass *bool `json:"options_preflight_bypass,omitempty"` - Type AccessApplicationType `json:"type,omitempty"` - AllowAuthenticateViaWarp *bool `json:"allow_authenticate_via_warp,omitempty"` - CustomPages []string `json:"custom_pages,omitempty"` - Tags []string `json:"tags,omitempty"` - SCIMConfig *AccessApplicationSCIMConfig `json:"scim_config,omitempty"` + AllowedIdps []string `json:"allowed_idps,omitempty"` + AppLauncherVisible *bool `json:"app_launcher_visible,omitempty"` + AUD string `json:"aud,omitempty"` + AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` + CorsHeaders *AccessApplicationCorsHeaders `json:"cors_headers,omitempty"` + CustomDenyMessage string `json:"custom_deny_message,omitempty"` + CustomDenyURL string `json:"custom_deny_url,omitempty"` + CustomNonIdentityDenyURL string `json:"custom_non_identity_deny_url,omitempty"` + Domain string `json:"domain"` + EnableBindingCookie *bool `json:"enable_binding_cookie,omitempty"` + GatewayRules []AccessApplicationGatewayRule `json:"gateway_rules,omitempty"` + HttpOnlyCookieAttribute *bool `json:"http_only_cookie_attribute,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + Name string `json:"name"` + PathCookieAttribute *bool `json:"path_cookie_attribute,omitempty"` + PrivateAddress string `json:"private_address"` + SaasApplication *SaasApplication `json:"saas_app,omitempty"` + SameSiteCookieAttribute string `json:"same_site_cookie_attribute,omitempty"` + SelfHostedDomains []string `json:"self_hosted_domains"` + ServiceAuth401Redirect *bool `json:"service_auth_401_redirect,omitempty"` + SessionDuration string `json:"session_duration,omitempty"` + SkipInterstitial *bool `json:"skip_interstitial,omitempty"` + OptionsPreflightBypass *bool `json:"options_preflight_bypass,omitempty"` + Type AccessApplicationType `json:"type,omitempty"` + AllowAuthenticateViaWarp *bool `json:"allow_authenticate_via_warp,omitempty"` + CustomPages []string `json:"custom_pages,omitempty"` + Tags []string `json:"tags,omitempty"` + SCIMConfig *AccessApplicationSCIMConfig `json:"scim_config,omitempty"` + TargetContexts *[]AccessInfrastructureTargetContext `json:"target_criteria,omitempty"` // List of policy ids to link to this application in ascending order of precedence. Policies []string `json:"policies,omitempty"` AccessAppLauncherCustomization } type UpdateAccessApplicationParams struct { - ID string `json:"id,omitempty"` - AllowedIdps []string `json:"allowed_idps,omitempty"` - AppLauncherVisible *bool `json:"app_launcher_visible,omitempty"` - AUD string `json:"aud,omitempty"` - AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` - CorsHeaders *AccessApplicationCorsHeaders `json:"cors_headers,omitempty"` - CustomDenyMessage string `json:"custom_deny_message,omitempty"` - CustomDenyURL string `json:"custom_deny_url,omitempty"` - CustomNonIdentityDenyURL string `json:"custom_non_identity_deny_url,omitempty"` - Domain string `json:"domain"` - EnableBindingCookie *bool `json:"enable_binding_cookie,omitempty"` - GatewayRules []AccessApplicationGatewayRule `json:"gateway_rules,omitempty"` - HttpOnlyCookieAttribute *bool `json:"http_only_cookie_attribute,omitempty"` - LogoURL string `json:"logo_url,omitempty"` - Name string `json:"name"` - PathCookieAttribute *bool `json:"path_cookie_attribute,omitempty"` - PrivateAddress string `json:"private_address"` - SaasApplication *SaasApplication `json:"saas_app,omitempty"` - SameSiteCookieAttribute string `json:"same_site_cookie_attribute,omitempty"` - SelfHostedDomains []string `json:"self_hosted_domains"` - ServiceAuth401Redirect *bool `json:"service_auth_401_redirect,omitempty"` - SessionDuration string `json:"session_duration,omitempty"` - SkipInterstitial *bool `json:"skip_interstitial,omitempty"` - Type AccessApplicationType `json:"type,omitempty"` - AllowAuthenticateViaWarp *bool `json:"allow_authenticate_via_warp,omitempty"` - OptionsPreflightBypass *bool `json:"options_preflight_bypass,omitempty"` - CustomPages []string `json:"custom_pages,omitempty"` - Tags []string `json:"tags,omitempty"` - SCIMConfig *AccessApplicationSCIMConfig `json:"scim_config,omitempty"` + ID string `json:"id,omitempty"` + AllowedIdps []string `json:"allowed_idps,omitempty"` + AppLauncherVisible *bool `json:"app_launcher_visible,omitempty"` + AUD string `json:"aud,omitempty"` + AutoRedirectToIdentity *bool `json:"auto_redirect_to_identity,omitempty"` + CorsHeaders *AccessApplicationCorsHeaders `json:"cors_headers,omitempty"` + CustomDenyMessage string `json:"custom_deny_message,omitempty"` + CustomDenyURL string `json:"custom_deny_url,omitempty"` + CustomNonIdentityDenyURL string `json:"custom_non_identity_deny_url,omitempty"` + Domain string `json:"domain"` + EnableBindingCookie *bool `json:"enable_binding_cookie,omitempty"` + GatewayRules []AccessApplicationGatewayRule `json:"gateway_rules,omitempty"` + HttpOnlyCookieAttribute *bool `json:"http_only_cookie_attribute,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + Name string `json:"name"` + PathCookieAttribute *bool `json:"path_cookie_attribute,omitempty"` + PrivateAddress string `json:"private_address"` + SaasApplication *SaasApplication `json:"saas_app,omitempty"` + SameSiteCookieAttribute string `json:"same_site_cookie_attribute,omitempty"` + SelfHostedDomains []string `json:"self_hosted_domains"` + ServiceAuth401Redirect *bool `json:"service_auth_401_redirect,omitempty"` + SessionDuration string `json:"session_duration,omitempty"` + SkipInterstitial *bool `json:"skip_interstitial,omitempty"` + Type AccessApplicationType `json:"type,omitempty"` + AllowAuthenticateViaWarp *bool `json:"allow_authenticate_via_warp,omitempty"` + OptionsPreflightBypass *bool `json:"options_preflight_bypass,omitempty"` + CustomPages []string `json:"custom_pages,omitempty"` + Tags []string `json:"tags,omitempty"` + SCIMConfig *AccessApplicationSCIMConfig `json:"scim_config,omitempty"` + TargetContexts *[]AccessInfrastructureTargetContext `json:"target_criteria,omitempty"` // List of policy ids to link to this application in ascending order of precedence. // Can reference reusable policies and policies specific to this application. // If this field is not provided, the existing policies will not be modified. diff --git a/access_application_test.go b/access_application_test.go index 1783863950b..50f2c7e6686 100644 --- a/access_application_test.go +++ b/access_application_test.go @@ -1011,7 +1011,7 @@ func TestCreateSAMLSaasAccessApplications(t *testing.T) { SPEntityID: "dash.example.com", NameIDFormat: "id", DefaultRelayState: "https://saas.example.com", - CustomAttributes: []SAMLAttributeConfig{ + CustomAttributes: &[]SAMLAttributeConfig{ { Name: "test1", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", @@ -1166,7 +1166,7 @@ func TestCreateOIDCSaasAccessApplications(t *testing.T) { AppLauncherURL: "https://saas.example.com", GroupFilterRegex: ".*", AllowPKCEWithoutClientSecret: BoolPtr(false), - CustomClaims: []OIDCClaimConfig{ + CustomClaims: &[]OIDCClaimConfig{ { Name: "test1", Source: SourceConfig{Name: "test1"}, @@ -1197,7 +1197,7 @@ func TestCreateOIDCSaasAccessApplications(t *testing.T) { AppLauncherURL: "https://saas.example.com", GroupFilterRegex: ".*", AllowPKCEWithoutClientSecret: BoolPtr(false), - CustomClaims: []OIDCClaimConfig{ + CustomClaims: &[]OIDCClaimConfig{ { Name: "test1", Source: SourceConfig{Name: "test1"}, @@ -1229,7 +1229,7 @@ func TestCreateOIDCSaasAccessApplications(t *testing.T) { AppLauncherURL: "https://saas.example.com", GroupFilterRegex: ".*", AllowPKCEWithoutClientSecret: BoolPtr(false), - CustomClaims: []OIDCClaimConfig{ + CustomClaims: &[]OIDCClaimConfig{ { Name: "test1", Source: SourceConfig{Name: "test1"}, @@ -1292,7 +1292,8 @@ func TestCreateApplicationWithAccessAppLauncherCustomization(t *testing.T) { "url": "https://somesite.com", "name": "bug" } - ] + ], + "skip_app_launcher_login_page": true } } `) @@ -1332,6 +1333,7 @@ func TestCreateApplicationWithAccessAppLauncherCustomization(t *testing.T) { Name: "bug", }, }, + SkipAppLauncherLoginPage: BoolPtr(true), }, } @@ -1357,6 +1359,7 @@ func TestCreateApplicationWithAccessAppLauncherCustomization(t *testing.T) { Name: "bug", }, }, + SkipAppLauncherLoginPage: BoolPtr(true), }, }) diff --git a/access_infrastructure_application.go b/access_infrastructure_application.go new file mode 100644 index 00000000000..3824400f2fc --- /dev/null +++ b/access_infrastructure_application.go @@ -0,0 +1,22 @@ +package cloudflare + +type AccessInfrastructureProtocol string + +const ( + AccessInfrastructureSSH AccessInfrastructureProtocol = "SSH" + AccessInfrastructureRDP AccessInfrastructureProtocol = "RDP" +) + +type AccessInfrastructureTargetContext struct { + TargetAttributes map[string][]string `json:"target_attributes"` + Port int `json:"port"` + Protocol AccessInfrastructureProtocol `json:"protocol"` +} + +type AccessInfrastructureConnectionRulesSSH struct { + Usernames []string `json:"usernames"` +} + +type AccessInfrastructureConnectionRules struct { + SSH *AccessInfrastructureConnectionRulesSSH `json:"ssh,omitempty"` +} diff --git a/access_infrastructure_application_test.go b/access_infrastructure_application_test.go new file mode 100644 index 00000000000..9b992b59677 --- /dev/null +++ b/access_infrastructure_application_test.go @@ -0,0 +1,358 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const testInfrastructureApplicationId = "f52d1f8f-f194-4ac1-845f-e47203bf4dbb" + +var ( + accessPolicyCreatedOn, _ = time.Parse(time.RFC3339, "2024-09-24T17:58:44Z") + accessPolicyUpdatedOn, _ = time.Parse(time.RFC3339, "2024-09-24T17:58:44Z") + infrastructureApplicationCreatedOn, _ = time.Parse(time.RFC3339, "2024-09-14T05:13:43Z") + infrastructureApplicationUpdatedOn, _ = time.Parse(time.RFC3339, "2024-09-24T17:58:44Z") + expectedInfraAppPolicy = AccessPolicy{ + ID: "8dd6af47-a80b-48cb-89c1-f39299ebf096", + Precedence: 1, + Reusable: BoolPtr(false), + Decision: "allow", + CreatedAt: &accessPolicyCreatedOn, + UpdatedAt: &accessPolicyUpdatedOn, + Name: "my infra SSH policy", + Include: []any{ + map[string]interface{}{"email": map[string]interface{}{"email": "devuser@cloudflare.com"}}, + }, + Exclude: []any{}, + Require: []any{}, + InfrastructureConnectionRules: &AccessInfrastructureConnectionRules{ + SSH: &AccessInfrastructureConnectionRulesSSH{ + Usernames: []string{"devuser"}, + }, + }, + } + expectedInfrastructureApplication = AccessApplication{ + ID: testInfrastructureApplicationId, + CreatedAt: &infrastructureApplicationCreatedOn, + UpdatedAt: &infrastructureApplicationUpdatedOn, + AUD: "6570722c13470d8c3e7aed619fc66cdf948002efbb06601166b8f2dad85fae34", + Name: "infrastructure_application_test", + Domain: "", + Type: "infrastructure", + SessionDuration: "", + AllowedIdps: nil, + AutoRedirectToIdentity: nil, + EnableBindingCookie: nil, + AppLauncherVisible: nil, + ServiceAuth401Redirect: nil, + CustomDenyMessage: "", + CustomDenyURL: "", + SameSiteCookieAttribute: "", + HttpOnlyCookieAttribute: nil, + LogoURL: "", + SkipInterstitial: nil, + PathCookieAttribute: nil, + CustomPages: nil, + Tags: nil, + CustomNonIdentityDenyURL: "", + AllowAuthenticateViaWarp: nil, + OptionsPreflightBypass: nil, + TargetContexts: &[]AccessInfrastructureTargetContext{ + { + TargetAttributes: map[string][]string{ + "hostname": {"cfgo-acc-tests"}, + }, + Port: 22, + Protocol: "SSH", + }, + }, + Policies: []AccessPolicy{ + expectedInfraAppPolicy, + }, + } + + expectedInfrastructureApplicationUpdated = AccessApplication{ + ID: testInfrastructureApplicationId, + CreatedAt: &infrastructureApplicationCreatedOn, + UpdatedAt: &infrastructureApplicationUpdatedOn, + AUD: "6570722c13470d8c3e7aed619fc66cdf948002efbb06601166b8f2dad85fae34", + Name: "infrastructure_application_test_updated", + Domain: "", + Type: "infrastructure", + SessionDuration: "", + AllowedIdps: nil, + AutoRedirectToIdentity: nil, + EnableBindingCookie: nil, + AppLauncherVisible: nil, + ServiceAuth401Redirect: nil, + CustomDenyMessage: "", + CustomDenyURL: "", + SameSiteCookieAttribute: "", + HttpOnlyCookieAttribute: nil, + LogoURL: "", + SkipInterstitial: nil, + PathCookieAttribute: nil, + CustomPages: nil, + Tags: nil, + CustomNonIdentityDenyURL: "", + AllowAuthenticateViaWarp: nil, + OptionsPreflightBypass: nil, + TargetContexts: &[]AccessInfrastructureTargetContext{ + { + TargetAttributes: map[string][]string{ + "hostname": {"cfgo-acc-tests", "cfgo-acc-tests-duplicate"}, + }, + Port: 22, + Protocol: "SSH", + }, + }, + Policies: []AccessPolicy{ + expectedInfraAppPolicy, + }, + } +) + +func TestInfrastructureApplication_Create(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/access/apps", 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": { + "id": "f52d1f8f-f194-4ac1-845f-e47203bf4dbb", + "type": "infrastructure", + "name": "infrastructure_application_test", + "aud": "6570722c13470d8c3e7aed619fc66cdf948002efbb06601166b8f2dad85fae34", + "created_at": "2024-09-14T05:13:43Z", + "updated_at": "2024-09-24T17:58:44Z", + "target_criteria": [ + { + "target_attributes": { + "hostname": [ "cfgo-acc-tests" ] + }, + "port": 22, + "protocol": "SSH" + } + ], + "policies": [ + { + "created_at": "2024-09-24T17:58:44Z", + "decision": "allow", + "exclude": [], + "id": "8dd6af47-a80b-48cb-89c1-f39299ebf096", + "include": [ + { + "email": { + "email": "devuser@cloudflare.com" + } + } + ], + "name": "my infra SSH policy", + "require": [], + "connection_rules": { + "ssh": { + "usernames": [ "devuser" ] + } + }, + "uid": "8dd6af47-a80b-48cb-89c1-f39299ebf096", + "updated_at": "2024-09-24T17:58:44Z", + "reusable": false, + "precedence": 1 + } + ] + } + }`) + }) + + actual, err := client.CreateAccessApplication(context.Background(), AccountIdentifier(testAccountID), CreateAccessApplicationParams{ + Name: "infrastructure_application_test", + Type: "infrastructure", + TargetContexts: &[]AccessInfrastructureTargetContext{ + { + TargetAttributes: map[string][]string{ + "hostname": {"cfgo-acc-tests"}, + }, + Port: 22, + Protocol: "SSH", + }, + }, + Policies: []string{ + "8dd6af47-a80b-48cb-89c1-f39299ebf096", + }, + }) + if assert.NoError(t, err) { + assert.Equal(t, expectedInfrastructureApplication, actual, "create infrastructure_application structs not equal") + } +} + +func TestInfrastructureApplication_Update(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/access/apps", 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": { + "id": "f52d1f8f-f194-4ac1-845f-e47203bf4dbb", + "type": "infrastructure", + "name": "infrastructure_application_test_updated", + "aud": "6570722c13470d8c3e7aed619fc66cdf948002efbb06601166b8f2dad85fae34", + "created_at": "2024-09-14T05:13:43Z", + "updated_at": "2024-09-24T17:58:44Z", + "target_criteria": [ + { + "target_attributes": { + "hostname": [ "cfgo-acc-tests", "cfgo-acc-tests-duplicate" ] + }, + "port": 22, + "protocol": "SSH" + } + ], + "policies": [ + { + "created_at": "2024-09-24T17:58:44Z", + "decision": "allow", + "exclude": [], + "id": "8dd6af47-a80b-48cb-89c1-f39299ebf096", + "include": [ + { + "email": { + "email": "devuser@cloudflare.com" + } + } + ], + "name": "my infra SSH policy", + "require": [], + "connection_rules": { + "ssh": { + "usernames": [ "devuser" ] + } + }, + "uid": "8dd6af47-a80b-48cb-89c1-f39299ebf096", + "updated_at": "2024-09-24T17:58:44Z", + "reusable": false, + "precedence": 1 + } + ] + } + }`) + }) + + actual, err := client.CreateAccessApplication(context.Background(), AccountIdentifier(testAccountID), CreateAccessApplicationParams{ + Name: "infrastructure_application_test_updated", + Type: "infrastructure", + TargetContexts: &[]AccessInfrastructureTargetContext{ + { + TargetAttributes: map[string][]string{ + "hostname": {"cfgo-acc-tests", "cfgo-acc-tests-duplicate"}, + }, + Port: 22, + Protocol: "SSH", + }, + }, + Policies: []string{ + "8dd6af47-a80b-48cb-89c1-f39299ebf096", + }, + }) + if assert.NoError(t, err) { + assert.Equal(t, expectedInfrastructureApplicationUpdated, actual, "create infrastructure_application structs not equal") + } +} + +func TestInfrastructureApplication_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/access/apps/f52d1f8f-f194-4ac1-845f-e47203bf4dbb", 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": "f52d1f8f-f194-4ac1-845f-e47203bf4dbb", + "type": "infrastructure", + "name": "infrastructure_application_test", + "aud": "6570722c13470d8c3e7aed619fc66cdf948002efbb06601166b8f2dad85fae34", + "created_at": "2024-09-14T05:13:43Z", + "updated_at": "2024-09-24T17:58:44Z", + "target_criteria": [ + { + "target_attributes": { + "hostname": [ "cfgo-acc-tests" ] + }, + "port": 22, + "protocol": "SSH" + } + ], + "policies": [ + { + "created_at": "2024-09-24T17:58:44Z", + "decision": "allow", + "exclude": [], + "id": "8dd6af47-a80b-48cb-89c1-f39299ebf096", + "include": [ + { + "email": { + "email": "devuser@cloudflare.com" + } + } + ], + "name": "my infra SSH policy", + "require": [], + "connection_rules": { + "ssh": { + "usernames": [ "devuser" ] + } + }, + "uid": "8dd6af47-a80b-48cb-89c1-f39299ebf096", + "updated_at": "2024-09-24T17:58:44Z", + "reusable": false, + "precedence": 1 + } + ] + } + }`) + }) + + actual, err := client.GetAccessApplication(context.Background(), AccountIdentifier(testAccountID), "f52d1f8f-f194-4ac1-845f-e47203bf4dbb") + if assert.NoError(t, err) { + assert.Equal(t, expectedInfrastructureApplication, actual, "get infrastructure_application structs not equal") + } +} + +func TestInfrastructureApplication_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/access/apps/f52d1f8f-f194-4ac1-845f-e47203bf4dbb", 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": "f52d1f8f-f194-4ac1-845f-e47203bf4dbb" + } + }`) + }) + + err := client.DeleteAccessApplication(context.Background(), AccountIdentifier(testAccountID), "f52d1f8f-f194-4ac1-845f-e47203bf4dbb") + assert.NoError(t, err) +} diff --git a/access_policy.go b/access_policy.go index a70ceede50c..2b2ec812d6d 100644 --- a/access_policy.go +++ b/access_policy.go @@ -30,12 +30,13 @@ type AccessPolicy struct { Reusable *bool `json:"reusable,omitempty"` Name string `json:"name"` - IsolationRequired *bool `json:"isolation_required,omitempty"` - SessionDuration *string `json:"session_duration,omitempty"` - PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` - PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` - ApprovalRequired *bool `json:"approval_required,omitempty"` - ApprovalGroups []AccessApprovalGroup `json:"approval_groups"` + IsolationRequired *bool `json:"isolation_required,omitempty"` + SessionDuration *string `json:"session_duration,omitempty"` + PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` + PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` + ApprovalRequired *bool `json:"approval_required,omitempty"` + ApprovalGroups []AccessApprovalGroup `json:"approval_groups"` + InfrastructureConnectionRules *AccessInfrastructureConnectionRules `json:"connection_rules,omitempty"` // The include policy works like an OR logical operator. The user must // satisfy one of the rules. @@ -94,12 +95,13 @@ type CreateAccessPolicyParams struct { Decision string `json:"decision"` Name string `json:"name"` - IsolationRequired *bool `json:"isolation_required,omitempty"` - SessionDuration *string `json:"session_duration,omitempty"` - PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` - PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` - ApprovalRequired *bool `json:"approval_required,omitempty"` - ApprovalGroups []AccessApprovalGroup `json:"approval_groups"` + IsolationRequired *bool `json:"isolation_required,omitempty"` + SessionDuration *string `json:"session_duration,omitempty"` + PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` + PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` + ApprovalRequired *bool `json:"approval_required,omitempty"` + ApprovalGroups []AccessApprovalGroup `json:"approval_groups"` + InfrastructureConnectionRules *AccessInfrastructureConnectionRules `json:"connection_rules,omitempty"` // The include policy works like an OR logical operator. The user must // satisfy one of the rules. @@ -127,12 +129,13 @@ type UpdateAccessPolicyParams struct { Decision string `json:"decision"` Name string `json:"name"` - IsolationRequired *bool `json:"isolation_required,omitempty"` - SessionDuration *string `json:"session_duration,omitempty"` - PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` - PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` - ApprovalRequired *bool `json:"approval_required,omitempty"` - ApprovalGroups []AccessApprovalGroup `json:"approval_groups"` + IsolationRequired *bool `json:"isolation_required,omitempty"` + SessionDuration *string `json:"session_duration,omitempty"` + PurposeJustificationRequired *bool `json:"purpose_justification_required,omitempty"` + PurposeJustificationPrompt *string `json:"purpose_justification_prompt,omitempty"` + ApprovalRequired *bool `json:"approval_required,omitempty"` + ApprovalGroups []AccessApprovalGroup `json:"approval_groups"` + InfrastructureConnectionRules *AccessInfrastructureConnectionRules `json:"connection_rules,omitempty"` // The include policy works like an OR logical operator. The user must // satisfy one of the rules. diff --git a/access_policy_test.go b/access_policy_test.go index 75642138205..6ed033fda01 100644 --- a/access_policy_test.go +++ b/access_policy_test.go @@ -55,6 +55,11 @@ var ( ApprovalsNeeded: 1, }, }, + InfrastructureConnectionRules: &AccessInfrastructureConnectionRules{ + SSH: &AccessInfrastructureConnectionRulesSSH{ + Usernames: []string{"root", "ec2-user"}, + }, + }, } ) @@ -115,7 +120,12 @@ func TestAccessPolicies(t *testing.T) { ], "approvals_needed": 1 } - ] + ], + "connection_rules": { + "ssh": { + "usernames": ["root", "ec2-user"] + } + } } ], "result_info": { @@ -214,7 +224,12 @@ func TestAccessPolicy(t *testing.T) { "email_addresses": ["email1@example.com", "email2@example.com"], "approvals_needed": 1 } - ] + ], + "connection_rules": { + "ssh": { + "usernames": ["root", "ec2-user"] + } + } } } `) @@ -307,7 +322,12 @@ func TestCreateAccessPolicy(t *testing.T) { "email_addresses": ["email1@example.com", "email2@example.com"], "approvals_needed": 1 } - ] + ], + "connection_rules": { + "ssh": { + "usernames": ["root", "ec2-user"] + } + } } } `) @@ -345,6 +365,11 @@ func TestCreateAccessPolicy(t *testing.T) { ApprovalsNeeded: 1, }, }, + InfrastructureConnectionRules: &AccessInfrastructureConnectionRules{ + SSH: &AccessInfrastructureConnectionRulesSSH{ + Usernames: []string{"root", "ec2-user"}, + }, + }, } mux.HandleFunc("/accounts/"+testAccountID+"/access/apps/"+accessApplicationID+"/policies", handler) @@ -415,6 +440,11 @@ func TestCreateAccessPolicyAuthContextRule(t *testing.T) { ApprovalsNeeded: 1, }, }, + InfrastructureConnectionRules: &AccessInfrastructureConnectionRules{ + SSH: &AccessInfrastructureConnectionRulesSSH{ + Usernames: []string{"root", "ec2-user"}, + }, + }, } handler := func(w http.ResponseWriter, r *http.Request) { @@ -462,7 +492,12 @@ func TestCreateAccessPolicyAuthContextRule(t *testing.T) { "email_addresses": ["email1@example.com", "email2@example.com"], "approvals_needed": 1 } - ] + ], + "connection_rules": { + "ssh": { + "usernames": ["root", "ec2-user"] + } + } } } `) @@ -554,6 +589,11 @@ func TestUpdateAccessPolicy(t *testing.T) { ApprovalsNeeded: 1, }, }, + InfrastructureConnectionRules: &AccessInfrastructureConnectionRules{ + SSH: &AccessInfrastructureConnectionRulesSSH{ + Usernames: []string{"root", "ec2-user"}, + }, + }, } handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) @@ -604,7 +644,12 @@ func TestUpdateAccessPolicy(t *testing.T) { "email_addresses": ["email1@example.com", "email2@example.com"], "approvals_needed": 1 } - ] + ], + "connection_rules": { + "ssh": { + "usernames": ["root", "ec2-user"] + } + } } } `) diff --git a/bot_management.go b/bot_management.go index f77968f4599..51205a6aea9 100644 --- a/bot_management.go +++ b/bot_management.go @@ -20,6 +20,7 @@ type BotManagement struct { SuppressSessionScore *bool `json:"suppress_session_score,omitempty"` AutoUpdateModel *bool `json:"auto_update_model,omitempty"` UsingLatestModel *bool `json:"using_latest_model,omitempty"` + AIBotsProtection *string `json:"ai_bots_protection,omitempty"` } // BotManagementResponse represents the response from the bot_management endpoint. @@ -38,6 +39,7 @@ type UpdateBotManagementParams struct { OptimizeWordpress *bool `json:"optimize_wordpress,omitempty"` SuppressSessionScore *bool `json:"suppress_session_score,omitempty"` AutoUpdateModel *bool `json:"auto_update_model,omitempty"` + AIBotsProtection *string `json:"ai_bots_protection,omitempty"` } // GetBotManagement gets a zone API shield configuration. diff --git a/bot_management_test.go b/bot_management_test.go index e0837804e5e..46ff1cff7f7 100644 --- a/bot_management_test.go +++ b/bot_management_test.go @@ -24,7 +24,8 @@ func TestGetBotManagement(t *testing.T) { "result": { "enable_js": false, "fight_mode": true, - "using_latest_model": true + "using_latest_model": true, + "ai_bots_protection": "disabled" } } `) @@ -36,6 +37,7 @@ func TestGetBotManagement(t *testing.T) { EnableJS: BoolPtr(false), FightMode: BoolPtr(true), UsingLatestModel: BoolPtr(true), + AIBotsProtection: StringPtr("disabled"), } actual, err := client.GetBotManagement(context.Background(), ZoneIdentifier(testZoneID)) @@ -60,7 +62,8 @@ func TestUpdateBotManagement(t *testing.T) { "result": { "enable_js": false, "fight_mode": true, - "using_latest_model": true + "using_latest_model": true, + "ai_bots_protection": "block" } } `) @@ -69,14 +72,16 @@ func TestUpdateBotManagement(t *testing.T) { mux.HandleFunc("/zones/"+testZoneID+"/bot_management", handler) bmData := UpdateBotManagementParams{ - EnableJS: BoolPtr(false), - FightMode: BoolPtr(true), + EnableJS: BoolPtr(false), + FightMode: BoolPtr(true), + AIBotsProtection: StringPtr("block"), } want := BotManagement{ EnableJS: BoolPtr(false), FightMode: BoolPtr(true), UsingLatestModel: BoolPtr(true), + AIBotsProtection: StringPtr("block"), } actual, err := client.UpdateBotManagement(context.Background(), ZoneIdentifier(testZoneID), bmData) diff --git a/cmd/flarectl/.goreleaser.yml b/cmd/flarectl/.goreleaser.yml index 131b233f2b5..86b765ad7d3 100644 --- a/cmd/flarectl/.goreleaser.yml +++ b/cmd/flarectl/.goreleaser.yml @@ -1,3 +1,4 @@ +version: 2 before: hooks: - cp ../../LICENSE . diff --git a/device_posture_rule.go b/device_posture_rule.go index 9d44526ed8c..066dadcb12b 100644 --- a/device_posture_rule.go +++ b/device_posture_rule.go @@ -204,6 +204,7 @@ type DevicePostureRuleInput struct { ExtendedKeyUsage []string `json:"extended_key_usage,omitempty"` CheckPrivateKey *bool `json:"check_private_key,omitempty"` Locations CertificateLocations `json:"locations,omitempty"` + Score int `json:"score,omitempty"` } // Locations struct for client certificate rule v2. diff --git a/dns.go b/dns.go index 14dbe2d4b3e..0c36efd429a 100644 --- a/dns.go +++ b/dns.go @@ -27,8 +27,6 @@ type DNSRecord struct { 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"` @@ -185,8 +183,6 @@ type CreateDNSRecordParams struct { 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"` diff --git a/dns_test.go b/dns_test.go index 43546dcc942..01af5c1b702 100644 --- a/dns_test.go +++ b/dns_test.go @@ -98,8 +98,6 @@ func TestCreateDNSRecord(t *testing.T) { "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -123,8 +121,6 @@ func TestCreateDNSRecord(t *testing.T) { Proxiable: true, Proxied: asciiInput.Proxied, TTL: asciiInput.TTL, - ZoneID: testZoneID, - ZoneName: "example.com", CreatedOn: createdOn, ModifiedOn: modifiedOn, Data: map[string]interface{}{}, @@ -179,8 +175,6 @@ func TestListDNSRecords(t *testing.T) { "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -212,8 +206,6 @@ func TestListDNSRecords(t *testing.T) { Proxiable: true, Proxied: &proxied, TTL: 120, - ZoneID: testZoneID, - ZoneName: "example.com", CreatedOn: createdOn, ModifiedOn: modifiedOn, Data: map[string]interface{}{}, @@ -272,8 +264,6 @@ func TestListDNSRecordsSearch(t *testing.T) { "proxiable": true, "proxied": true, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -306,8 +296,6 @@ func TestListDNSRecordsSearch(t *testing.T) { Proxiable: true, Proxied: &proxied, TTL: 120, - ZoneID: testZoneID, - ZoneName: "example.com", CreatedOn: createdOn, ModifiedOn: modifiedOn, Data: map[string]interface{}{}, @@ -403,8 +391,6 @@ func TestListDNSRecordsPagination(t *testing.T) { 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["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) } @@ -430,8 +416,6 @@ func TestGetDNSRecord(t *testing.T) { "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -460,8 +444,6 @@ func TestGetDNSRecord(t *testing.T) { Proxiable: true, Proxied: &proxied, TTL: 120, - ZoneID: testZoneID, - ZoneName: "example.com", CreatedOn: createdOn, ModifiedOn: modifiedOn, Data: map[string]interface{}{}, @@ -521,8 +503,6 @@ func TestUpdateDNSRecord(t *testing.T) { "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -586,8 +566,6 @@ func TestUpdateDNSRecord_ClearComment(t *testing.T) { "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "comment":null, @@ -642,8 +620,6 @@ func TestUpdateDNSRecord_KeepComment(t *testing.T) { "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "comment":null, diff --git a/errors.go b/errors.go index 85fdc3efacb..93faab0dbd6 100644 --- a/errors.go +++ b/errors.go @@ -152,6 +152,10 @@ func (e RequestError) Type() ErrorType { return e.cloudflareError.Type } +func (e RequestError) Unwrap() error { + return e.cloudflareError +} + func NewRequestError(e *Error) RequestError { return RequestError{ cloudflareError: e, @@ -192,6 +196,10 @@ func (e RatelimitError) Type() ErrorType { return e.cloudflareError.Type } +func (e RatelimitError) Unwrap() error { + return e.cloudflareError +} + func NewRatelimitError(e *Error) RatelimitError { return RatelimitError{ cloudflareError: e, @@ -231,6 +239,10 @@ func (e ServiceError) Type() ErrorType { return e.cloudflareError.Type } +func (e ServiceError) Unwrap() error { + return e.cloudflareError +} + func NewServiceError(e *Error) ServiceError { return ServiceError{ cloudflareError: e, @@ -270,6 +282,10 @@ func (e AuthenticationError) Type() ErrorType { return e.cloudflareError.Type } +func (e AuthenticationError) Unwrap() error { + return e.cloudflareError +} + func NewAuthenticationError(e *Error) AuthenticationError { return AuthenticationError{ cloudflareError: e, @@ -309,6 +325,10 @@ func (e AuthorizationError) Type() ErrorType { return e.cloudflareError.Type } +func (e AuthorizationError) Unwrap() error { + return e.cloudflareError +} + func NewAuthorizationError(e *Error) AuthorizationError { return AuthorizationError{ cloudflareError: e, @@ -348,6 +368,10 @@ func (e NotFoundError) Type() ErrorType { return e.cloudflareError.Type } +func (e NotFoundError) Unwrap() error { + return e.cloudflareError +} + func NewNotFoundError(e *Error) NotFoundError { return NotFoundError{ cloudflareError: e, diff --git a/go.mod b/go.mod index c2e4badccba..ddc60219b3e 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/google/go-querystring v1.1.0 github.com/olekukonko/tablewriter v0.0.5 github.com/stretchr/testify v1.9.0 - github.com/urfave/cli/v2 v2.27.3 - golang.org/x/net v0.27.0 - golang.org/x/time v0.5.0 + github.com/urfave/cli/v2 v2.27.4 + golang.org/x/net v0.30.0 + golang.org/x/time v0.7.0 ) require gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect @@ -25,6 +25,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-20240521201337-686a1a2994c1 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 09d6cc2e914..8f04a34b265 100644 --- a/go.sum +++ b/go.sum @@ -35,16 +35,16 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= -github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= diff --git a/infrastructure_access_target.go b/infrastructure_access_target.go new file mode 100644 index 00000000000..0c6a72a8666 --- /dev/null +++ b/infrastructure_access_target.go @@ -0,0 +1,234 @@ +package cloudflare + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/goccy/go-json" +) + +var ErrMissingTargetId = errors.New("required target id missing") + +type InfrastructureAccessTarget struct { + Hostname string `json:"hostname"` + ID string `json:"id"` + IP InfrastructureAccessTargetIPInfo `json:"ip"` + CreatedAt string `json:"created_at"` + ModifiedAt string `json:"modified_at"` +} + +type InfrastructureAccessTargetIPInfo struct { + IPV4 *InfrastructureAccessTargetIPDetails `json:"ipv4,omitempty"` + IPV6 *InfrastructureAccessTargetIPDetails `json:"ipv6,omitempty"` +} + +type InfrastructureAccessTargetIPDetails struct { + IPAddr string `json:"ip_addr"` + VirtualNetworkId string `json:"virtual_network_id"` +} + +type InfrastructureAccessTargetParams struct { + Hostname string `json:"hostname"` + IP InfrastructureAccessTargetIPInfo `json:"ip"` +} + +type CreateInfrastructureAccessTargetParams struct { + InfrastructureAccessTargetParams +} + +type UpdateInfrastructureAccessTargetParams struct { + ID string `json:"-"` + ModifyParams InfrastructureAccessTargetParams `json:"modify_params"` +} + +// InfrastructureAccessTargetDetailResponse is the API response, containing a single target. +type InfrastructureAccessTargetDetailResponse struct { + Result InfrastructureAccessTarget `json:"result"` + Response +} + +type InfrastructureAccessTargetListDetailResponse struct { + Result []InfrastructureAccessTarget `json:"result"` + Response + ResultInfo `json:"result_info"` +} + +type InfrastructureAccessTargetListParams struct { + Hostname string `url:"hostname,omitempty"` + HostnameContains string `url:"hostname_contains,omitempty"` + IPV4 string `url:"ip_v4,omitempty"` + IPV6 string `url:"ip_v6,omitempty"` + CreatedAfter string `url:"created_after,omitempty"` + ModifedAfter string `url:"modified_after,omitempty"` + VirtualNetworkId string `url:"virtual_network_id,omitempty"` + + ResultInfo +} + +// ListInfrastructureAccessTargets returns all infrastructure access targets within an account. +// +// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-list +func (api *API) ListInfrastructureAccessTargets(ctx context.Context, rc *ResourceContainer, params InfrastructureAccessTargetListParams) ([]InfrastructureAccessTarget, *ResultInfo, error) { + if rc.Identifier == "" { + return []InfrastructureAccessTarget{}, &ResultInfo{}, ErrMissingAccountID + } + + baseURL := fmt.Sprintf("/%s/%s/infrastructure/targets", rc.Level, rc.Identifier) + + 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 targets []InfrastructureAccessTarget + var r InfrastructureAccessTargetListDetailResponse + + for { + uri := buildURI(baseURL, params) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []InfrastructureAccessTarget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + err = json.Unmarshal(res, &r) + if err != nil { + return []InfrastructureAccessTarget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + targets = append(targets, r.Result...) + params.ResultInfo = r.ResultInfo.Next() + if params.ResultInfo.Done() || !autoPaginate { + break + } + } + + return targets, &r.ResultInfo, nil +} + +// CreateInfrastructureAccessTarget creates a new infrastructure access target. +// +// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-post +func (api *API) CreateInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, params CreateInfrastructureAccessTargetParams) (InfrastructureAccessTarget, error) { + if rc.Identifier == "" { + return InfrastructureAccessTarget{}, ErrMissingAccountID + } + + uri := fmt.Sprintf("/%s/%s/infrastructure/targets", rc.Level, rc.Identifier) + + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var targetDetailResponse InfrastructureAccessTargetDetailResponse + err = json.Unmarshal(res, &targetDetailResponse) + if err != nil { + return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return targetDetailResponse.Result, nil +} + +// UpdateInfrastructureAccessTarget updates an existing infrastructure access target. +// +// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-put +func (api *API) UpdateInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, params UpdateInfrastructureAccessTargetParams) (InfrastructureAccessTarget, error) { + if rc.Identifier == "" { + return InfrastructureAccessTarget{}, ErrMissingAccountID + } + + if params.ID == "" { + return InfrastructureAccessTarget{}, ErrMissingTargetId + } + + uri := fmt.Sprintf( + "/%s/%s/infrastructure/targets/%s", + rc.Level, + rc.Identifier, + params.ID, + ) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params.ModifyParams) + if err != nil { + return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var targetDetailResponse InfrastructureAccessTargetDetailResponse + err = json.Unmarshal(res, &targetDetailResponse) + if err != nil { + return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return targetDetailResponse.Result, nil +} + +// GetInfrastructureAccessTarget returns a single infrastructure access target based on target ID +// ID for either account or zone. +// +// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-get +func (api *API) GetInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, targetID string) (InfrastructureAccessTarget, error) { + if rc.Identifier == "" { + return InfrastructureAccessTarget{}, ErrMissingAccountID + } + + if targetID == "" { + return InfrastructureAccessTarget{}, ErrMissingTargetId + } + + uri := fmt.Sprintf( + "/%s/%s/infrastructure/targets/%s", + rc.Level, + rc.Identifier, + targetID, + ) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + var targetDetailResponse InfrastructureAccessTargetDetailResponse + err = json.Unmarshal(res, &targetDetailResponse) + if err != nil { + return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return targetDetailResponse.Result, nil +} + +// DeleteInfrastructureAccessTarget deletes an infrastructure access target. +// +// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-delete +func (api *API) DeleteInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, targetID string) error { + if rc.Identifier == "" { + return ErrMissingAccountID + } + + if targetID == "" { + return ErrMissingTargetId + } + + uri := fmt.Sprintf( + "/%s/%s/infrastructure/targets/%s", + rc.Level, + rc.Identifier, + targetID, + ) + + _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return fmt.Errorf("%s: %w", errMakeRequestError, err) + } + + return nil +} diff --git a/infrastructure_access_target_test.go b/infrastructure_access_target_test.go new file mode 100644 index 00000000000..dc1423ff035 --- /dev/null +++ b/infrastructure_access_target_test.go @@ -0,0 +1,286 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const testInfrastructureAccessTargetId = "019205b5-97d7-7272-b00e-0ea05e61a124" + +var ( + infrastructureAccessTargetCreatedOn, _ = time.Parse(time.RFC3339, "2024-08-25T05:00:22Z") + infrastructureAccessTargetModifiedOn, _ = time.Parse(time.RFC3339, "2024-08-25T05:00:22Z") + expectedInfrastructureAccessTarget = InfrastructureAccessTarget{ + Hostname: "infra-access-target", + ID: testInfrastructureAccessTargetId, + IP: InfrastructureAccessTargetIPInfo{ + IPV4: &InfrastructureAccessTargetIPDetails{ + IPAddr: "187.26.29.249", + VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55", + }, + IPV6: &InfrastructureAccessTargetIPDetails{ + IPAddr: "2001:0db8:0000:0000:0000:0000:0000:1000", + VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55", + }, + }, + CreatedAt: infrastructureAccessTargetCreatedOn.String(), + ModifiedAt: infrastructureAccessTargetModifiedOn.String(), + } + expectedInfrastructureModified = InfrastructureAccessTarget{ + Hostname: "infra-access-target-modified", + ID: testInfrastructureAccessTargetId, + IP: InfrastructureAccessTargetIPInfo{ + IPV4: &InfrastructureAccessTargetIPDetails{ + IPAddr: "198.51.100.2", + VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55", + }, + }, + CreatedAt: infrastructureAccessTargetCreatedOn.String(), + ModifiedAt: infrastructureAccessTargetModifiedOn.String(), + } +) + +func TestInfrastructureAccessTarget_Create(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets", 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": { + "created_at": "2024-08-25 05:00:22 +0000 UTC", + "hostname": "infra-access-target", + "id": "019205b5-97d7-7272-b00e-0ea05e61a124", + "ip": { + "ipv4": { + "ip_addr": "187.26.29.249", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + }, + "ipv6": { + "ip_addr": "2001:0db8:0000:0000:0000:0000:0000:1000", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + } + }, + "modified_at": "2024-08-25 05:00:22 +0000 UTC" + } + }`) + }) + + // Make sure missing account ID is thrown + _, err := client.CreateInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), CreateInfrastructureAccessTargetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + out, err := client.CreateInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), CreateInfrastructureAccessTargetParams{ + InfrastructureAccessTargetParams: InfrastructureAccessTargetParams{ + Hostname: "infra-access-target", + IP: InfrastructureAccessTargetIPInfo{ + IPV4: &InfrastructureAccessTargetIPDetails{ + IPAddr: "187.26.29.249", + VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55", + }, + IPV6: &InfrastructureAccessTargetIPDetails{ + IPAddr: "2001:0db8:0000:0000:0000:0000:0000:1000", + VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55", + }, + }, + }, + }) + if assert.NoError(t, err) { + assert.Equal(t, expectedInfrastructureAccessTarget, out, "create infrastructure_access_target structs not equal") + } +} + +func TestInfrastructureTarget_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets", 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": [ + { + "created_at": "2024-08-25 05:00:22 +0000 UTC", + "hostname": "infra-access-target", + "id": "019205b5-97d7-7272-b00e-0ea05e61a124", + "ip": { + "ipv4": { + "ip_addr": "187.26.29.249", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + }, + "ipv6": { + "ip_addr": "2001:0db8:0000:0000:0000:0000:0000:1000", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + } + }, + "modified_at": "2024-08-25 05:00:22 +0000 UTC" + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 1 + } +}`) + }) + + _, _, err := client.ListInfrastructureAccessTargets(context.Background(), AccountIdentifier(""), InfrastructureAccessTargetListParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + out, results, err := client.ListInfrastructureAccessTargets(context.Background(), AccountIdentifier(testAccountID), InfrastructureAccessTargetListParams{}) + 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, expectedInfrastructureAccessTarget, out[0], "list infrastructure_access_target structs not equal") + } +} + +func TestInfrastructureTarget_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets/"+testInfrastructureAccessTargetId, 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": { + "created_at": "2024-08-25 05:00:22 +0000 UTC", + "hostname": "infra-access-target", + "id": "019205b5-97d7-7272-b00e-0ea05e61a124", + "ip": { + "ipv4": { + "ip_addr": "187.26.29.249", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + }, + "ipv6": { + "ip_addr": "2001:0db8:0000:0000:0000:0000:0000:1000", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + } + }, + "modified_at": "2024-08-25 05:00:22 +0000 UTC" + } +}`) + }) + + _, err := client.GetInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.GetInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingTargetId, err) + } + + out, err := client.GetInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), testInfrastructureAccessTargetId) + + if assert.NoError(t, err) { + assert.Equal(t, expectedInfrastructureAccessTarget, out, "get infrastructure_target not equal to expected") + } +} + +func TestInfrastructureTarget_Update(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets/"+testInfrastructureAccessTargetId, 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": { + "created_at": "2024-08-25 05:00:22 +0000 UTC", + "hostname": "infra-access-target-modified", + "id": "019205b5-97d7-7272-b00e-0ea05e61a124", + "ip": { + "ipv4": { + "ip_addr": "198.51.100.2", + "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55" + } + }, + "modified_at": "2024-08-25 05:00:22 +0000 UTC" + } +}`) + }) + + _, err := client.UpdateInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), UpdateInfrastructureAccessTargetParams{}) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + _, err = client.UpdateInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), UpdateInfrastructureAccessTargetParams{ + ID: "", + ModifyParams: InfrastructureAccessTargetParams{}, + }) + if assert.Error(t, err) { + assert.Equal(t, ErrMissingTargetId, err) + } + + out, err := client.UpdateInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), UpdateInfrastructureAccessTargetParams{ + ID: testInfrastructureAccessTargetId, + ModifyParams: InfrastructureAccessTargetParams{ + // Updates hostname and IPv4 address. Deletes IPv6 address. + Hostname: "infra-access-target-modified", + IP: InfrastructureAccessTargetIPInfo{ + IPV4: &InfrastructureAccessTargetIPDetails{ + IPAddr: "198.51.100.2", + VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55", + }, + }, + }, + }) + if assert.NoError(t, err) { + assert.Equal(t, expectedInfrastructureModified, out, "update infrastructure_access_target structs not equal") + } +} + +func TestInfrastructureTarget_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets/"+testInfrastructureAccessTargetId, 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, ``) + }) + + // Make sure missing account ID is thrown + err := client.DeleteInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingAccountID, err) + } + + err = client.DeleteInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), "") + if assert.Error(t, err) { + assert.Equal(t, ErrMissingTargetId, err) + } + + err = client.DeleteInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), testInfrastructureAccessTargetId) + assert.NoError(t, err) +} diff --git a/load_balancing.go b/load_balancing.go index 44d7fd9fa33..6100b5eaf58 100644 --- a/load_balancing.go +++ b/load_balancing.go @@ -101,7 +101,7 @@ type LoadBalancer struct { CreatedOn *time.Time `json:"created_on,omitempty"` ModifiedOn *time.Time `json:"modified_on,omitempty"` Description string `json:"description"` - Name string `json:"name"` + Name string `json:"name,omitempty"` TTL int `json:"ttl,omitempty"` FallbackPool string `json:"fallback_pool"` DefaultPools []string `json:"default_pools"` @@ -144,6 +144,11 @@ type LoadBalancer struct { // // "": Maps to "geo" if RegionPools or PopPools or CountryPools have entries otherwise "off". SteeringPolicy string `json:"steering_policy,omitempty"` + + // TunnelID is the ID of the tunnel associated with this load balancer. + // It is only used for account load balancers and has no relation to any + // tunnels used as origins for this load balancer. + TunnelID string `json:"tunnel_id,omitempty"` } // LoadBalancerLoadShedding contains the settings for controlling load shedding. @@ -681,12 +686,15 @@ func (api *API) UpdateLoadBalancerMonitor(ctx context.Context, rc *ResourceConta // // API reference: https://api.cloudflare.com/#load-balancers-create-load-balancer func (api *API) CreateLoadBalancer(ctx context.Context, rc *ResourceContainer, params CreateLoadBalancerParams) (LoadBalancer, error) { - if rc.Level != ZoneRouteLevel { + + var uri string + switch rc.Level { + case AccountRouteLevel, ZoneRouteLevel: + uri = fmt.Sprintf("/%s/%s/load_balancers", rc.Level, rc.Identifier) + default: return LoadBalancer{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) } - uri := fmt.Sprintf("/zones/%s/load_balancers", rc.Identifier) - res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params.LoadBalancer) if err != nil { return LoadBalancer{}, err @@ -702,11 +710,17 @@ func (api *API) CreateLoadBalancer(ctx context.Context, rc *ResourceContainer, p // // API reference: https://api.cloudflare.com/#load-balancers-list-load-balancers func (api *API) ListLoadBalancers(ctx context.Context, rc *ResourceContainer, params ListLoadBalancerParams) ([]LoadBalancer, error) { - if rc.Level != ZoneRouteLevel { - return []LoadBalancer{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) + + var uri string + switch rc.Level { + case AccountRouteLevel, ZoneRouteLevel: + uri = fmt.Sprintf("/%s/%s/load_balancers", rc.Level, rc.Identifier) + default: + return nil, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) } - uri := buildURI(fmt.Sprintf("/zones/%s/load_balancers", rc.Identifier), params.PaginationOptions) + // add pagination options to the URI + uri = buildURI(uri, params.PaginationOptions) res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { @@ -723,7 +737,12 @@ func (api *API) ListLoadBalancers(ctx context.Context, rc *ResourceContainer, pa // // API reference: https://api.cloudflare.com/#load-balancers-load-balancer-details func (api *API) GetLoadBalancer(ctx context.Context, rc *ResourceContainer, loadbalancerID string) (LoadBalancer, error) { - if rc.Level != ZoneRouteLevel { + + var uri string + switch rc.Level { + case AccountRouteLevel, ZoneRouteLevel: + uri = fmt.Sprintf("/%s/%s/load_balancers/%s", rc.Level, rc.Identifier, loadbalancerID) + default: return LoadBalancer{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) } @@ -731,8 +750,6 @@ func (api *API) GetLoadBalancer(ctx context.Context, rc *ResourceContainer, load return LoadBalancer{}, ErrMissingLoadBalancerID } - uri := fmt.Sprintf("/zones/%s/load_balancers/%s", rc.Identifier, loadbalancerID) - res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) if err != nil { return LoadBalancer{}, err @@ -748,7 +765,12 @@ func (api *API) GetLoadBalancer(ctx context.Context, rc *ResourceContainer, load // // API reference: https://api.cloudflare.com/#load-balancers-delete-load-balancer func (api *API) DeleteLoadBalancer(ctx context.Context, rc *ResourceContainer, loadbalancerID string) error { - if rc.Level != ZoneRouteLevel { + + var uri string + switch rc.Level { + case AccountRouteLevel, ZoneRouteLevel: + uri = fmt.Sprintf("/%s/%s/load_balancers/%s", rc.Level, rc.Identifier, loadbalancerID) + default: return fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) } @@ -756,8 +778,6 @@ func (api *API) DeleteLoadBalancer(ctx context.Context, rc *ResourceContainer, l return ErrMissingLoadBalancerID } - uri := fmt.Sprintf("/zones/%s/load_balancers/%s", rc.Identifier, loadbalancerID) - if _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil); err != nil { return err } @@ -768,7 +788,12 @@ func (api *API) DeleteLoadBalancer(ctx context.Context, rc *ResourceContainer, l // // API reference: https://api.cloudflare.com/#load-balancers-update-load-balancer func (api *API) UpdateLoadBalancer(ctx context.Context, rc *ResourceContainer, params UpdateLoadBalancerParams) (LoadBalancer, error) { - if rc.Level != ZoneRouteLevel { + + var uri string + switch rc.Level { + case AccountRouteLevel, ZoneRouteLevel: + uri = fmt.Sprintf("/%s/%s/load_balancers/%s", rc.Level, rc.Identifier, params.LoadBalancer.ID) + default: return LoadBalancer{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level) } @@ -776,8 +801,6 @@ func (api *API) UpdateLoadBalancer(ctx context.Context, rc *ResourceContainer, p return LoadBalancer{}, ErrMissingLoadBalancerID } - uri := fmt.Sprintf("/zones/%s/load_balancers/%s", rc.Identifier, params.LoadBalancer.ID) - res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params.LoadBalancer) if err != nil { return LoadBalancer{}, err diff --git a/load_balancing_test.go b/load_balancing_test.go index 3a9ab5afb5b..e4e2ae20431 100644 --- a/load_balancing_test.go +++ b/load_balancing_test.go @@ -1449,17 +1449,815 @@ func TestCreateLoadBalancer(t *testing.T) { } } -func TestCreateLoadBalancer_AccountIsNotSupported(t *testing.T) { +func TestCreateAccountLoadBalancer(t *testing.T) { setup() defer teardown() - _, err := client.CreateLoadBalancer(context.Background(), AccountIdentifier(testAccountID), CreateLoadBalancerParams{}) - if assert.Error(t, err) { - assert.Equal(t, fmt.Sprintf(errInvalidResourceContainerAccess, AccountRouteLevel), err.Error()) + 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") + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{ + "description": "Account Load Balancer", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "abd90f38ced07c2e2f4df50b1f61d4194" + ] + }, + "pop_pools": { + "LAX": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.2, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + } + }, + "adaptive_routing": { + "failover_across_pools": true + }, + "location_strategy": { + "prefer_ecs": "always", + "mode": "resolver_ip" + }, + "rules": [ + { + "name": "example rule", + "condition": "cf.load_balancer.region == \"SAF\"", + "disabled": false, + "priority": 0, + "overrides": { + "region_pools": { + "SAF": ["de90f38ced07c2e2f4df50b1f61d4194"] + }, + "adaptive_routing": { + "failover_across_pools": false + }, + "location_strategy": { + "prefer_ecs": "never", + "mode": "pop" + } + } + } + ], + "proxied": true, + "session_affinity": "cookie", + "session_affinity_ttl": 5000, + "session_affinity_attributes": { + "samesite": "Strict", + "secure": "Always", + "drain_duration": 60, + "zero_downtime_failover": "sticky" + } + }`, string(b)) + } + + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "699d98642c564d2e855e9661899b7252", + "created_on": "2014-01-01T05:20:00.12345Z", + "modified_on": "2014-02-01T05:20:00.12345Z", + "description": "Account Load Balancer", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "abd90f38ced07c2e2f4df50b1f61d4194" + ] + }, + "pop_pools": { + "LAX": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.2, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + } + }, + "adaptive_routing": { + "failover_across_pools": true + }, + "location_strategy": { + "prefer_ecs": "always", + "mode": "resolver_ip" + }, + "rules": [ + { + "name": "example rule", + "condition": "cf.load_balancer.region == \"SAF\"", + "overrides": { + "region_pools": { + "SAF": ["de90f38ced07c2e2f4df50b1f61d4194"] + }, + "adaptive_routing": { + "failover_across_pools": false + }, + "location_strategy": { + "prefer_ecs": "never", + "mode": "pop" + } + } + } + ], + "proxied": true, + "session_affinity": "cookie", + "session_affinity_ttl": 5000, + "session_affinity_attributes": { + "samesite": "Strict", + "secure": "Always", + "drain_duration": 60, + "zero_downtime_failover": "sticky" + } + } + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/load_balancers", handler) + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-02-01T05:20:00.12345Z") + want := LoadBalancer{ + ID: "699d98642c564d2e855e9661899b7252", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + Description: "Account Load Balancer", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + CountryPools: map[string][]string{ + "US": { + "de90f38ced07c2e2f4df50b1f61d4194", + }, + "GB": { + "abd90f38ced07c2e2f4df50b1f61d4194", + }, + }, + PopPools: map[string][]string{ + "LAX": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "LHR": { + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", + }, + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.2, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4, + }, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(true), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "always", + Mode: "resolver_ip", + }, + Rules: []*LoadBalancerRule{ + { + Name: "example rule", + Condition: "cf.load_balancer.region == \"SAF\"", + Overrides: LoadBalancerRuleOverrides{ + RegionPools: map[string][]string{ + "SAF": {"de90f38ced07c2e2f4df50b1f61d4194"}, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(false), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "never", + Mode: "pop", + }, + }, + }, + }, + Proxied: true, + Persistence: "cookie", + PersistenceTTL: 5000, + SessionAffinityAttributes: &SessionAffinityAttributes{ + SameSite: "Strict", + Secure: "Always", + DrainDuration: 60, + ZeroDowntimeFailover: "sticky", + }, + } + request := LoadBalancer{ + Description: "Account Load Balancer", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + CountryPools: map[string][]string{ + "US": { + "de90f38ced07c2e2f4df50b1f61d4194", + }, + "GB": { + "abd90f38ced07c2e2f4df50b1f61d4194", + }, + }, + PopPools: map[string][]string{ + "LAX": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "LHR": { + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", + }, + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.2, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4, + }, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(true), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "always", + Mode: "resolver_ip", + }, + Rules: []*LoadBalancerRule{ + { + Name: "example rule", + Condition: "cf.load_balancer.region == \"SAF\"", + Overrides: LoadBalancerRuleOverrides{ + RegionPools: map[string][]string{ + "SAF": {"de90f38ced07c2e2f4df50b1f61d4194"}, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(false), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "never", + Mode: "pop", + }, + }, + }, + }, + Proxied: true, + Persistence: "cookie", + PersistenceTTL: 5000, + SessionAffinityAttributes: &SessionAffinityAttributes{ + SameSite: "Strict", + Secure: "Always", + DrainDuration: 60, + ZeroDowntimeFailover: "sticky", + }, + } + + actual, err := client.CreateLoadBalancer(context.Background(), AccountIdentifier(testAccountID), CreateLoadBalancerParams{LoadBalancer: request}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListLoadBalancers(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": "699d98642c564d2e855e9661899b7252", + "created_on": "2014-01-01T05:20:00.12345Z", + "modified_on": "2014-02-01T05:20:00.12345Z", + "description": "Load Balancer for www.example.com", + "name": "www.example.com", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "abd90f38ced07c2e2f4df50b1f61d4194" + ] + }, + "pop_pools": { + "LAX": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.2, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + } + }, + "adaptive_routing": { + "failover_across_pools": true + }, + "location_strategy": { + "prefer_ecs": "always", + "mode": "resolver_ip" + }, + "proxied": true + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 1 + } + }`) + } + + mux.HandleFunc("/zones/"+testZoneID+"/load_balancers", handler) + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-02-01T05:20:00.12345Z") + want := []LoadBalancer{ + { + ID: "699d98642c564d2e855e9661899b7252", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + Description: "Load Balancer for www.example.com", + Name: "www.example.com", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + CountryPools: map[string][]string{ + "US": { + "de90f38ced07c2e2f4df50b1f61d4194", + }, + "GB": { + "abd90f38ced07c2e2f4df50b1f61d4194", + }, + }, + PopPools: map[string][]string{ + "LAX": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "LHR": { + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", + }, + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.2, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4, + }, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(true), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "always", + Mode: "resolver_ip", + }, + Proxied: true, + }, + } + + actual, err := client.ListLoadBalancers(context.Background(), ZoneIdentifier(testZoneID), ListLoadBalancerParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListAccountLoadBalancers(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": "699d98642c564d2e855e9661899b7252", + "created_on": "2014-01-01T05:20:00.12345Z", + "modified_on": "2014-02-01T05:20:00.12345Z", + "description": "Account Load Balancer", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "abd90f38ced07c2e2f4df50b1f61d4194" + ] + }, + "pop_pools": { + "LAX": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.2, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + } + }, + "adaptive_routing": { + "failover_across_pools": true + }, + "location_strategy": { + "prefer_ecs": "always", + "mode": "resolver_ip" + }, + "proxied": true + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 1 + } + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/load_balancers", handler) + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-02-01T05:20:00.12345Z") + want := []LoadBalancer{ + { + ID: "699d98642c564d2e855e9661899b7252", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + Description: "Account Load Balancer", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + CountryPools: map[string][]string{ + "US": { + "de90f38ced07c2e2f4df50b1f61d4194", + }, + "GB": { + "abd90f38ced07c2e2f4df50b1f61d4194", + }, + }, + PopPools: map[string][]string{ + "LAX": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "LHR": { + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", + }, + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.2, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4, + }, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(true), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "always", + Mode: "resolver_ip", + }, + Proxied: true, + }, + } + + actual, err := client.ListLoadBalancers(context.Background(), AccountIdentifier(testAccountID), ListLoadBalancerParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetLoadBalancer(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": "699d98642c564d2e855e9661899b7252", + "created_on": "2014-01-01T05:20:00.12345Z", + "modified_on": "2014-02-01T05:20:00.12345Z", + "description": "Load Balancer for www.example.com", + "name": "www.example.com", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "abd90f38ced07c2e2f4df50b1f61d4194" + ] + }, + "pop_pools": { + "LAX": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.2, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + } + }, + "adaptive_routing": { + "failover_across_pools": true + }, + "location_strategy": { + "prefer_ecs": "always", + "mode": "resolver_ip" + }, + "proxied": true + } + }`) + } + + mux.HandleFunc("/zones/"+testZoneID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-02-01T05:20:00.12345Z") + want := LoadBalancer{ + ID: "699d98642c564d2e855e9661899b7252", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + Description: "Load Balancer for www.example.com", + Name: "www.example.com", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + CountryPools: map[string][]string{ + "US": { + "de90f38ced07c2e2f4df50b1f61d4194", + }, + "GB": { + "abd90f38ced07c2e2f4df50b1f61d4194", + }, + }, + PopPools: map[string][]string{ + "LAX": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "LHR": { + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", + }, + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.2, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4, + }, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(true), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "always", + Mode: "resolver_ip", + }, + Proxied: true, + } + + actual, err := client.GetLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "699d98642c564d2e855e9661899b7252") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) } + + _, err = client.GetLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "bar") + assert.Error(t, err) } -func TestListLoadBalancers(t *testing.T) { +func TestGetAccountLoadBalancer(t *testing.T) { setup() defer teardown() @@ -1470,164 +2268,250 @@ func TestListLoadBalancers(t *testing.T) { "success": true, "errors": [], "messages": [], - "result": [ - { - "id": "699d98642c564d2e855e9661899b7252", - "created_on": "2014-01-01T05:20:00.12345Z", - "modified_on": "2014-02-01T05:20:00.12345Z", - "description": "Load Balancer for www.example.com", - "name": "www.example.com", - "ttl": 30, - "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", - "default_pools": [ - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196", - "00920f38ce07c2e2f4df50b1f61d4194" - ], - "region_pools": { - "WNAM": [ - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196" - ], - "ENAM": [ - "00920f38ce07c2e2f4df50b1f61d4194" - ] - }, - "country_pools": { - "US": [ - "de90f38ced07c2e2f4df50b1f61d4194" - ], - "GB": [ - "abd90f38ced07c2e2f4df50b1f61d4194" - ] - }, - "pop_pools": { - "LAX": [ - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196" - ], - "LHR": [ - "abd90f38ced07c2e2f4df50b1f61d4194", - "f9138c5d07c2e2f4df57b1f61d4196" - ], - "SJC": [ - "00920f38ce07c2e2f4df50b1f61d4194" - ] - }, - "random_steering": { - "default_weight": 0.2, - "pool_weights": { - "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, - "de90f38ced07c2e2f4df50b1f61d4194": 0.4 - } - }, - "adaptive_routing": { - "failover_across_pools": true - }, - "location_strategy": { - "prefer_ecs": "always", - "mode": "resolver_ip" - }, - "proxied": true - } - ], - "result_info": { - "page": 1, - "per_page": 20, - "count": 1, - "total_count": 1 + "result": { + "id": "699d98642c564d2e855e9661899b7252", + "created_on": "2014-01-01T05:20:00.12345Z", + "modified_on": "2014-02-01T05:20:00.12345Z", + "description": "Account Load Balancer", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "abd90f38ced07c2e2f4df50b1f61d4194" + ] + }, + "pop_pools": { + "LAX": [ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.2, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + } + }, + "adaptive_routing": { + "failover_across_pools": true + }, + "location_strategy": { + "prefer_ecs": "always", + "mode": "resolver_ip" + }, + "proxied": true } }`) } - mux.HandleFunc("/zones/"+testZoneID+"/load_balancers", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") modifiedOn, _ := time.Parse(time.RFC3339, "2014-02-01T05:20:00.12345Z") - want := []LoadBalancer{ - { - ID: "699d98642c564d2e855e9661899b7252", - CreatedOn: &createdOn, - ModifiedOn: &modifiedOn, - Description: "Load Balancer for www.example.com", - Name: "www.example.com", - TTL: 30, - FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", - DefaultPools: []string{ + want := LoadBalancer{ + ID: "699d98642c564d2e855e9661899b7252", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + Description: "Account Load Balancer", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { "de90f38ced07c2e2f4df50b1f61d4194", "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { "00920f38ce07c2e2f4df50b1f61d4194", }, - RegionPools: map[string][]string{ - "WNAM": { - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196", - }, - "ENAM": { - "00920f38ce07c2e2f4df50b1f61d4194", - }, + }, + CountryPools: map[string][]string{ + "US": { + "de90f38ced07c2e2f4df50b1f61d4194", }, - CountryPools: map[string][]string{ - "US": { - "de90f38ced07c2e2f4df50b1f61d4194", - }, - "GB": { - "abd90f38ced07c2e2f4df50b1f61d4194", - }, + "GB": { + "abd90f38ced07c2e2f4df50b1f61d4194", }, - PopPools: map[string][]string{ - "LAX": { - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196", - }, - "LHR": { - "abd90f38ced07c2e2f4df50b1f61d4194", - "f9138c5d07c2e2f4df57b1f61d4196", - }, - "SJC": { - "00920f38ce07c2e2f4df50b1f61d4194", - }, + }, + PopPools: map[string][]string{ + "LAX": { + "de90f38ced07c2e2f4df50b1f61d4194", + "9290f38c5d07c2e2f4df57b1f61d4196", }, - RandomSteering: &RandomSteering{ - DefaultWeight: 0.2, - PoolWeights: map[string]float64{ - "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, - "de90f38ced07c2e2f4df50b1f61d4194": 0.4, - }, + "LHR": { + "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", }, - AdaptiveRouting: &AdaptiveRouting{ - FailoverAcrossPools: BoolPtr(true), + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", }, - LocationStrategy: &LocationStrategy{ - PreferECS: "always", - Mode: "resolver_ip", + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.2, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, + "de90f38ced07c2e2f4df50b1f61d4194": 0.4, }, - Proxied: true, }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(true), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "always", + Mode: "resolver_ip", + }, + Proxied: true, + } + + actual, err := client.GetLoadBalancer(context.Background(), AccountIdentifier(testAccountID), "699d98642c564d2e855e9661899b7252") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.GetLoadBalancer(context.Background(), AccountIdentifier(testAccountID), "bar") + assert.Error(t, err) +} + +func TestDeleteLoadBalancer(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": "699d98642c564d2e855e9661899b7252" + } + }`) } - actual, err := client.ListLoadBalancers(context.Background(), ZoneIdentifier(testZoneID), ListLoadBalancerParams{}) - if assert.NoError(t, err) { - assert.Equal(t, want, actual) - } + mux.HandleFunc("/zones/"+testZoneID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) + assert.NoError(t, client.DeleteLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "699d98642c564d2e855e9661899b7252")) + assert.Error(t, client.DeleteLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "bar")) } -func TestListLoadBalancer_AccountIsNotSupported(t *testing.T) { +func TestDeleteAccountLoadBalancer(t *testing.T) { setup() defer teardown() - _, err := client.ListLoadBalancers(context.Background(), AccountIdentifier(testAccountID), ListLoadBalancerParams{}) - if assert.Error(t, err) { - assert.Equal(t, fmt.Sprintf(errInvalidResourceContainerAccess, AccountRouteLevel), err.Error()) + 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": "699d98642c564d2e855e9661899b7252" + } + }`) } + + mux.HandleFunc("/accounts/"+testAccountID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) + assert.NoError(t, client.DeleteLoadBalancer(context.Background(), AccountIdentifier(testAccountID), "699d98642c564d2e855e9661899b7252")) + assert.Error(t, client.DeleteLoadBalancer(context.Background(), AccountIdentifier(testAccountID), "bar")) } -func TestGetLoadBalancer(t *testing.T) { +func TestUpdateLoadBalancer(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) + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) w.Header().Set("content-type", "application/json") + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{ + "id": "699d98642c564d2e855e9661899b7252", + "description": "Load Balancer for www.example.com", + "name": "www.example.com", + "ttl": 30, + "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", + "default_pools": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ], + "region_pools": { + "WNAM": [ + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "ENAM": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "country_pools": { + "US": [ + "de90f38ced07c2e2f4df50b1f61d4194" + ], + "GB": [ + "f9138c5d07c2e2f4df57b1f61d4196" + ] + }, + "pop_pools": { + "LAX": [ + "9290f38c5d07c2e2f4df57b1f61d4196" + ], + "LHR": [ + "f9138c5d07c2e2f4df57b1f61d4196" + ], + "SJC": [ + "00920f38ce07c2e2f4df50b1f61d4194" + ] + }, + "random_steering": { + "default_weight": 0.5, + "pool_weights": { + "9290f38c5d07c2e2f4df57b1f61d4196": 0.2 + } + }, + "adaptive_routing": { + "failover_across_pools": false + }, + "location_strategy": { + "prefer_ecs": "never", + "mode": "pop" + }, + "proxied": true, + "session_affinity": "none", + "session_affinity_attributes": { + "samesite": "Strict", + "secure": "Always", + "zero_downtime_failover": "sticky" + } + }`, string(b)) + } fmt.Fprint(w, `{ "success": true, "errors": [], @@ -1635,19 +2519,16 @@ func TestGetLoadBalancer(t *testing.T) { "result": { "id": "699d98642c564d2e855e9661899b7252", "created_on": "2014-01-01T05:20:00.12345Z", - "modified_on": "2014-02-01T05:20:00.12345Z", + "modified_on": "2017-02-01T05:20:00.12345Z", "description": "Load Balancer for www.example.com", "name": "www.example.com", "ttl": 30, "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", "default_pools": [ - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196", "00920f38ce07c2e2f4df50b1f61d4194" ], "region_pools": { "WNAM": [ - "de90f38ced07c2e2f4df50b1f61d4194", "9290f38c5d07c2e2f4df57b1f61d4196" ], "ENAM": [ @@ -1659,16 +2540,14 @@ func TestGetLoadBalancer(t *testing.T) { "de90f38ced07c2e2f4df50b1f61d4194" ], "GB": [ - "abd90f38ced07c2e2f4df50b1f61d4194" + "f9138c5d07c2e2f4df57b1f61d4196" ] }, "pop_pools": { "LAX": [ - "de90f38ced07c2e2f4df50b1f61d4194", "9290f38c5d07c2e2f4df57b1f61d4196" ], "LHR": [ - "abd90f38ced07c2e2f4df50b1f61d4194", "f9138c5d07c2e2f4df57b1f61d4196" ], "SJC": [ @@ -1676,27 +2555,32 @@ func TestGetLoadBalancer(t *testing.T) { ] }, "random_steering": { - "default_weight": 0.2, + "default_weight": 0.5, "pool_weights": { - "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, - "de90f38ced07c2e2f4df50b1f61d4194": 0.4 + "9290f38c5d07c2e2f4df57b1f61d4196": 0.2 } }, "adaptive_routing": { - "failover_across_pools": true + "failover_across_pools": false }, "location_strategy": { - "prefer_ecs": "always", - "mode": "resolver_ip" + "prefer_ecs": "never", + "mode": "pop" }, - "proxied": true + "proxied": true, + "session_affinity": "none", + "session_affinity_attributes": { + "samesite": "Strict", + "secure": "Always", + "zero_downtime_failover": "sticky" + } } }`) } mux.HandleFunc("/zones/"+testZoneID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") - modifiedOn, _ := time.Parse(time.RFC3339, "2014-02-01T05:20:00.12345Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2017-02-01T05:20:00.12345Z") want := LoadBalancer{ ID: "699d98642c564d2e855e9661899b7252", CreatedOn: &createdOn, @@ -1706,13 +2590,10 @@ func TestGetLoadBalancer(t *testing.T) { TTL: 30, FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", DefaultPools: []string{ - "de90f38ced07c2e2f4df50b1f61d4194", - "9290f38c5d07c2e2f4df57b1f61d4196", "00920f38ce07c2e2f4df50b1f61d4194", }, RegionPools: map[string][]string{ "WNAM": { - "de90f38ced07c2e2f4df50b1f61d4194", "9290f38c5d07c2e2f4df57b1f61d4196", }, "ENAM": { @@ -1724,16 +2605,71 @@ func TestGetLoadBalancer(t *testing.T) { "de90f38ced07c2e2f4df50b1f61d4194", }, "GB": { - "abd90f38ced07c2e2f4df50b1f61d4194", + "f9138c5d07c2e2f4df57b1f61d4196", }, }, PopPools: map[string][]string{ "LAX": { + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "LHR": { + "f9138c5d07c2e2f4df57b1f61d4196", + }, + "SJC": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + RandomSteering: &RandomSteering{ + DefaultWeight: 0.5, + PoolWeights: map[string]float64{ + "9290f38c5d07c2e2f4df57b1f61d4196": 0.2, + }, + }, + AdaptiveRouting: &AdaptiveRouting{ + FailoverAcrossPools: BoolPtr(false), + }, + LocationStrategy: &LocationStrategy{ + PreferECS: "never", + Mode: "pop", + }, + Proxied: true, + Persistence: "none", + SessionAffinityAttributes: &SessionAffinityAttributes{ + SameSite: "Strict", + Secure: "Always", + ZeroDowntimeFailover: "sticky", + }, + } + request := LoadBalancer{ + ID: "699d98642c564d2e855e9661899b7252", + Description: "Load Balancer for www.example.com", + Name: "www.example.com", + TTL: 30, + FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", + DefaultPools: []string{ + "00920f38ce07c2e2f4df50b1f61d4194", + }, + RegionPools: map[string][]string{ + "WNAM": { + "9290f38c5d07c2e2f4df57b1f61d4196", + }, + "ENAM": { + "00920f38ce07c2e2f4df50b1f61d4194", + }, + }, + CountryPools: map[string][]string{ + "US": { "de90f38ced07c2e2f4df50b1f61d4194", + }, + "GB": { + "f9138c5d07c2e2f4df57b1f61d4196", + }, + }, + PopPools: map[string][]string{ + "LAX": { "9290f38c5d07c2e2f4df57b1f61d4196", }, "LHR": { - "abd90f38ced07c2e2f4df50b1f61d4194", "f9138c5d07c2e2f4df57b1f61d4196", }, "SJC": { @@ -1741,74 +2677,34 @@ func TestGetLoadBalancer(t *testing.T) { }, }, RandomSteering: &RandomSteering{ - DefaultWeight: 0.2, + DefaultWeight: 0.5, PoolWeights: map[string]float64{ - "9290f38c5d07c2e2f4df57b1f61d4196": 0.6, - "de90f38ced07c2e2f4df50b1f61d4194": 0.4, + "9290f38c5d07c2e2f4df57b1f61d4196": 0.2, }, }, AdaptiveRouting: &AdaptiveRouting{ - FailoverAcrossPools: BoolPtr(true), + FailoverAcrossPools: BoolPtr(false), }, LocationStrategy: &LocationStrategy{ - PreferECS: "always", - Mode: "resolver_ip", + PreferECS: "never", + Mode: "pop", + }, + Proxied: true, + Persistence: "none", + SessionAffinityAttributes: &SessionAffinityAttributes{ + SameSite: "Strict", + Secure: "Always", + ZeroDowntimeFailover: "sticky", }, - Proxied: true, } - actual, err := client.GetLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "699d98642c564d2e855e9661899b7252") + actual, err := client.UpdateLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), UpdateLoadBalancerParams{LoadBalancer: request}) if assert.NoError(t, err) { assert.Equal(t, want, actual) } - - _, err = client.GetLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "bar") - assert.Error(t, err) -} - -func TestGetLoadBalancer_AccountIsNotSupported(t *testing.T) { - setup() - defer teardown() - - _, err := client.GetLoadBalancer(context.Background(), AccountIdentifier(testAccountID), "foo") - if assert.Error(t, err) { - assert.Equal(t, fmt.Sprintf(errInvalidResourceContainerAccess, AccountRouteLevel), err.Error()) - } -} - -func TestDeleteLoadBalancer(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": "699d98642c564d2e855e9661899b7252" - } - }`) - } - - mux.HandleFunc("/zones/"+testZoneID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) - assert.NoError(t, client.DeleteLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "699d98642c564d2e855e9661899b7252")) - assert.Error(t, client.DeleteLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), "bar")) -} - -func TestDeleteLoadBalancer_AccountIsNotSupported(t *testing.T) { - setup() - defer teardown() - - err := client.DeleteLoadBalancer(context.Background(), AccountIdentifier(testAccountID), "foo") - if assert.Error(t, err) { - assert.Equal(t, fmt.Sprintf(errInvalidResourceContainerAccess, AccountRouteLevel), err.Error()) - } } -func TestUpdateLoadBalancer(t *testing.T) { +func TestUpdateAccountLoadBalancer(t *testing.T) { setup() defer teardown() @@ -1820,8 +2716,7 @@ func TestUpdateLoadBalancer(t *testing.T) { if assert.NoError(t, err) { assert.JSONEq(t, `{ "id": "699d98642c564d2e855e9661899b7252", - "description": "Load Balancer for www.example.com", - "name": "www.example.com", + "description": "Account Load Balancer", "ttl": 30, "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", "default_pools": [ @@ -1884,8 +2779,7 @@ func TestUpdateLoadBalancer(t *testing.T) { "id": "699d98642c564d2e855e9661899b7252", "created_on": "2014-01-01T05:20:00.12345Z", "modified_on": "2017-02-01T05:20:00.12345Z", - "description": "Load Balancer for www.example.com", - "name": "www.example.com", + "description": "Account Load Balancer", "ttl": 30, "fallback_pool": "17b5962d775c646f3f9725cbc7a53df4", "default_pools": [ @@ -1942,15 +2836,14 @@ func TestUpdateLoadBalancer(t *testing.T) { }`) } - mux.HandleFunc("/zones/"+testZoneID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) + mux.HandleFunc("/accounts/"+testAccountID+"/load_balancers/699d98642c564d2e855e9661899b7252", handler) createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") modifiedOn, _ := time.Parse(time.RFC3339, "2017-02-01T05:20:00.12345Z") want := LoadBalancer{ ID: "699d98642c564d2e855e9661899b7252", CreatedOn: &createdOn, ModifiedOn: &modifiedOn, - Description: "Load Balancer for www.example.com", - Name: "www.example.com", + Description: "Account Load Balancer", TTL: 30, FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", DefaultPools: []string{ @@ -2006,8 +2899,7 @@ func TestUpdateLoadBalancer(t *testing.T) { } request := LoadBalancer{ ID: "699d98642c564d2e855e9661899b7252", - Description: "Load Balancer for www.example.com", - Name: "www.example.com", + Description: "Account Load Balancer", TTL: 30, FallbackPool: "17b5962d775c646f3f9725cbc7a53df4", DefaultPools: []string{ @@ -2062,22 +2954,12 @@ func TestUpdateLoadBalancer(t *testing.T) { }, } - actual, err := client.UpdateLoadBalancer(context.Background(), ZoneIdentifier(testZoneID), UpdateLoadBalancerParams{LoadBalancer: request}) + actual, err := client.UpdateLoadBalancer(context.Background(), AccountIdentifier(testAccountID), UpdateLoadBalancerParams{LoadBalancer: request}) if assert.NoError(t, err) { assert.Equal(t, want, actual) } } -func TestUpdateLoadBalancer_AccountIsNotSupported(t *testing.T) { - setup() - defer teardown() - - _, err := client.UpdateLoadBalancer(context.Background(), AccountIdentifier(testAccountID), UpdateLoadBalancerParams{LoadBalancer: LoadBalancer{}}) - if assert.Error(t, err) { - assert.Equal(t, fmt.Sprintf(errInvalidResourceContainerAccess, AccountRouteLevel), err.Error()) - } -} - func TestLoadBalancerPoolHealthDetails(t *testing.T) { setup() defer teardown() diff --git a/rulesets.go b/rulesets.go index 8b1ad430a56..1573bebb00d 100644 --- a/rulesets.go +++ b/rulesets.go @@ -334,7 +334,8 @@ type RulesetRuleActionParametersCustomKey struct { type RulesetRuleActionParametersCustomKeyHeader struct { RulesetRuleActionParametersCustomKeyFields - ExcludeOrigin *bool `json:"exclude_origin,omitempty"` + ExcludeOrigin *bool `json:"exclude_origin,omitempty"` + Contains map[string][]string `json:"contains,omitempty"` } type RulesetRuleActionParametersCustomKeyCookie RulesetRuleActionParametersCustomKeyFields @@ -741,6 +742,11 @@ type UpdateEntrypointRulesetParams struct { Rules []RulesetRule `json:"rules"` } +type DeleteRulesetRuleParams struct { + RulesetID string `json:"-"` + RulesetRuleID string `json:"-"` +} + // ListRulesets lists all Rulesets in a given zone or account depending on the // ResourceContainer type provided. // @@ -844,6 +850,27 @@ func (api *API) UpdateRuleset(ctx context.Context, rc *ResourceContainer, params return result.Result, nil } +// DeleteRulesetRule removes a ruleset rule based on the ruleset ID + +// ruleset rule ID. +// +// API reference: https://developers.cloudflare.com/api/operations/deleteZoneRulesetRule +func (api *API) DeleteRulesetRule(ctx context.Context, rc *ResourceContainer, params DeleteRulesetRuleParams) error { + uri := fmt.Sprintf("/%s/%s/rulesets/%s/rules/%s", rc.Level, rc.Identifier, params.RulesetID, params.RulesetRuleID) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return err + } + + // The API is not implementing the standard response blob but returns an + // empty response (204) in case of a success. So we are checking for the + // response body size here. + if len(res) > 0 { + return fmt.Errorf(errMakeRequestError+": %w", errors.New(string(res))) + } + + return nil +} + // GetEntrypointRuleset returns an entry point ruleset base on the phase. // // API reference: https://developers.cloudflare.com/api/operations/getAccountEntrypointRuleset diff --git a/rulesets_test.go b/rulesets_test.go index dbaf2b4f139..941f935b1de 100644 --- a/rulesets_test.go +++ b/rulesets_test.go @@ -229,7 +229,7 @@ func TestGetRuleset_SetCacheSettings(t *testing.T) { "ignore_query_strings_order":true, "custom_key": { "query_string":{"include":"*"}, - "header":{"include":["habc","hdef"],"check_presence":["hfizz","hbuzz"],"exclude_origin":true}, + "header":{"include":["habc","hdef"],"check_presence":["hfizz","hbuzz"],"exclude_origin":true,"contains":{"accept":["image/web", "image/png"]}}, "cookie":{"include":["cabc","cdef"],"check_presence":["cfizz","cbuzz"]}, "user":{ "device_type":true, @@ -316,6 +316,9 @@ func TestGetRuleset_SetCacheSettings(t *testing.T) { CheckPresence: []string{"hfizz", "hbuzz"}, }, ExcludeOrigin: BoolPtr(true), + Contains: map[string][]string{ + "accept": {"image/web", "image/png"}, + }, }, Cookie: &RulesetRuleActionParametersCustomKeyCookie{ Include: []string{"cabc", "cdef"}, diff --git a/scripts/generate-changelog.sh b/scripts/generate-changelog.sh index dbd0c3775f5..a9748eef161 100755 --- a/scripts/generate-changelog.sh +++ b/scripts/generate-changelog.sh @@ -24,7 +24,7 @@ then exit 1 fi -CHANGELOG=$($(go env GOPATH)/bin/changelog-build -this-release $TARGET_SHA \ +CHANGELOG=$(changelog-build -this-release $TARGET_SHA \ -last-release $PREVIOUS_RELEASE_SHA \ -git-dir $__parent \ -entries-dir .changelog \ diff --git a/teams_certificates.go b/teams_certificates.go index 74267866944..00e6f1063ad 100644 --- a/teams_certificates.go +++ b/teams_certificates.go @@ -10,7 +10,7 @@ import ( ) type TeamsCertificate struct { - Enabled *bool `json:"enabled"` + InUse *bool `json:"in_use"` ID string `json:"id"` BindingStatus string `json:"binding_status"` QsPackId string `json:"qs_pack_id"` diff --git a/teams_certificates_test.go b/teams_certificates_test.go index 8fc7155bbf4..6a9d57a75f2 100644 --- a/teams_certificates_test.go +++ b/teams_certificates_test.go @@ -23,7 +23,7 @@ func TestTeamsCertificate(t *testing.T) { "errors": [], "messages": [], "result": { - "enabled": false, + "in_use": false, "id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4", "binding_status": "inactive", "qs_pack_id": "00000000-0000-0000-0000-000000000000", @@ -42,7 +42,7 @@ func TestTeamsCertificate(t *testing.T) { actual, err := client.TeamsCertificate(context.Background(), testAccountID, testCertID) if assert.NoError(t, err) { - assert.Equal(t, *actual.Enabled, false) + assert.Equal(t, *actual.InUse, false) assert.Equal(t, actual.ID, testCertID) assert.Equal(t, actual.BindingStatus, "inactive") assert.Equal(t, actual.Type, "gateway_managed") @@ -62,7 +62,7 @@ func TestTeamsCertificatesList(t *testing.T) { "messages": [], "result": [ { - "enabled": false, + "in_use": false, "id": "43a36083-987b-4321-95ab-4052771c4e6f", "binding_status": "inactive", "qs_pack_id": "00000000-0000-0000-0000-000000000000", @@ -73,7 +73,7 @@ func TestTeamsCertificatesList(t *testing.T) { "expires_on": "2122-10-29T16:59:47Z" }, { - "enabled": false, + "in_use": false, "id": "4a9d6ecf-0fdd-4676-818a-ee6b45f17f9b", "binding_status": "inactive", "qs_pack_id": "00000000-0000-0000-0000-000000000000", @@ -111,7 +111,7 @@ func TestTeamsAccountGenerateCertificate(t *testing.T) { "errors": [], "messages": [], "result": { - "enabled": false, + "in_use": false, "id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4", "binding_status": "inactive", "qs_pack_id": "00000000-0000-0000-0000-000000000000", @@ -149,7 +149,7 @@ func TestTeamsActivateCertificate(t *testing.T) { "errors": [], "messages": [], "result": { - "enabled": false, + "in_use": false, "id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4", "binding_status": "pending_deployment", "qs_pack_id": "00000000-0000-0000-0000-000000000000", @@ -183,7 +183,7 @@ func TestTeamsDeactivateCertificate(t *testing.T) { "errors": [], "messages": [], "result": { - "enabled": false, + "in_use": false, "id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4", "binding_status": "pending_deletion", "qs_pack_id": "00000000-0000-0000-0000-000000000000", diff --git a/testdata/fixtures/dns/list_page_1.json b/testdata/fixtures/dns/list_page_1.json index 5ff33aac68c..ce6ef07b698 100644 --- a/testdata/fixtures/dns/list_page_1.json +++ b/testdata/fixtures/dns/list_page_1.json @@ -11,8 +11,6 @@ "proxiable": true, "proxied": true, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -33,8 +31,6 @@ "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -55,8 +51,6 @@ "proxiable": true, "proxied": true, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, diff --git a/testdata/fixtures/dns/list_page_2.json b/testdata/fixtures/dns/list_page_2.json index f783aa44971..73cf683e749 100644 --- a/testdata/fixtures/dns/list_page_2.json +++ b/testdata/fixtures/dns/list_page_2.json @@ -11,8 +11,6 @@ "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, @@ -33,8 +31,6 @@ "proxiable": true, "proxied": false, "ttl": 120, - "zone_id": "d56084adb405e0b7e32c52321bf07be6", - "zone_name": "example.com", "created_on": "2014-01-01T05:20:00Z", "modified_on": "2014-01-01T05:20:00Z", "data": {}, diff --git a/testdata/fixtures/tunnel/single_full.json b/testdata/fixtures/tunnel/single_full.json index e5178e650f1..6eb38f30ccf 100644 --- a/testdata/fixtures/tunnel/single_full.json +++ b/testdata/fixtures/tunnel/single_full.json @@ -1,25 +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" - } - ], - "status": "healthy", - "tun_type": "cfd_tunnel", - "remote_config": true - } -} +{ + "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/waiting_room.go b/waiting_room.go index 7bd98d9a339..0cb787b4ce5 100644 --- a/waiting_room.go +++ b/waiting_room.go @@ -39,6 +39,7 @@ type WaitingRoom struct { CookieSuffix string `json:"cookie_suffix"` AdditionalRoutes []*WaitingRoomRoute `json:"additional_routes,omitempty"` QueueingStatusCode int `json:"queueing_status_code"` + EnabledOriginCommands []string `json:"enabled_origin_commands,omitempty"` } // WaitingRoomStatus describes the status of a waiting room. diff --git a/waiting_room_test.go b/waiting_room_test.go index 4d5ccf00e04..26f729af70b 100644 --- a/waiting_room_test.go +++ b/waiting_room_test.go @@ -40,9 +40,10 @@ var waitingRoomJSON = fmt.Sprintf(` "default_template_language": "en-US", "next_event_prequeue_start_time": null, "next_event_start_time": "%s", - "cookie_suffix": "example_shop", - "additional_routes": [{"host": "shop2.example.com", "path": "/shop/checkout"}], - "queueing_status_code": 200 + "cookie_suffix": "example_shop", + "additional_routes": [{"host": "shop2.example.com", "path": "/shop/checkout"}], + "queueing_status_code": 200, + "enabled_origin_commands": ["revoke"] } `, waitingRoomID, testTimestampWaitingRoom.Format(time.RFC3339Nano), testTimestampWaitingRoom.Format(time.RFC3339Nano), testTimestampWaitingRoomEventStart.Format(time.RFC3339Nano)) @@ -129,6 +130,7 @@ var waitingRoom = WaitingRoom{ CookieSuffix: "example_shop", AdditionalRoutes: []*WaitingRoomRoute{{Host: "shop2.example.com", Path: "/shop/checkout"}}, QueueingStatusCode: 200, + EnabledOriginCommands: []string{"revoke"}, } var waitingRoomEvent = WaitingRoomEvent{ diff --git a/workers_bindings.go b/workers_bindings.go index 0516c5b3f72..04916ce6897 100644 --- a/workers_bindings.go +++ b/workers_bindings.go @@ -46,6 +46,8 @@ const ( DispatchNamespaceBindingType WorkerBindingType = "dispatch_namespace" // WorkerD1DataseBindingType is for D1 databases. WorkerD1DataseBindingType WorkerBindingType = "d1" + // WorkerHyperdriveBindingType is for Hyperdrive config bindings. + WorkerHyperdriveBindingType WorkerBindingType = "hyperdrive" ) type ListWorkerBindingsParams struct { @@ -433,6 +435,32 @@ func (b WorkerD1DatabaseBinding) serialize(bindingName string) (workerBindingMet }, nil, nil } +// WorkerHyperdriveBinding is a binding to a Hyperdrive config. +type WorkerHyperdriveBinding struct { + Binding string + ConfigID string +} + +// Type returns the type of the binding. +func (b WorkerHyperdriveBinding) Type() WorkerBindingType { + return WorkerHyperdriveBindingType +} + +func (b WorkerHyperdriveBinding) 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.ConfigID == "" { + return nil, nil, fmt.Errorf(`config ID for binding "%s" cannot be empty`, bindingName) + } + + return workerBindingMeta{ + "name": b.Binding, + "type": b.Type(), + "id": b.ConfigID, + }, nil, nil +} + // UnsafeBinding is for experimental or deprecated bindings, and allows specifying any binding type or property. type UnsafeBinding map[string]interface{} @@ -562,6 +590,12 @@ func (api *API) ListWorkerBindings(ctx context.Context, rc *ResourceContainer, p bindingListItem.Binding = WorkerD1DatabaseBinding{ DatabaseID: database_id, } + case WorkerHyperdriveBindingType: + id := jsonBinding["id"].(string) + bindingListItem.Binding = WorkerHyperdriveBinding{ + Binding: name, + ConfigID: id, + } default: bindingListItem.Binding = WorkerInheritBinding{} } diff --git a/workers_bindings_test.go b/workers_bindings_test.go index bb7f141fa8b..bcd1b001bd5 100644 --- a/workers_bindings_test.go +++ b/workers_bindings_test.go @@ -35,7 +35,7 @@ func TestListWorkerBindings(t *testing.T) { assert.NoError(t, err) assert.Equal(t, successResponse, res.Response) - assert.Equal(t, 9, len(res.BindingList)) + assert.Equal(t, 10, len(res.BindingList)) assert.Equal(t, res.BindingList[0], WorkerBindingListItem{ Name: "MY_KV", @@ -106,6 +106,15 @@ func TestListWorkerBindings(t *testing.T) { }, }) assert.Equal(t, WorkerD1DataseBindingType, res.BindingList[8].Binding.Type()) + + assert.Equal(t, res.BindingList[9], WorkerBindingListItem{ + Name: "MY_HYPERDRIVE", + Binding: WorkerHyperdriveBinding{ + Binding: "MY_HYPERDRIVE", + ConfigID: "aaf4609248cc493cbc8d3e446e38fdfa", + }, + }) + assert.Equal(t, WorkerHyperdriveBindingType, res.BindingList[9].Binding.Type()) } func TestListWorkerBindings_Wfp(t *testing.T) { @@ -125,7 +134,7 @@ func TestListWorkerBindings_Wfp(t *testing.T) { assert.NoError(t, err) assert.Equal(t, successResponse, res.Response) - assert.Equal(t, 9, len(res.BindingList)) + assert.Equal(t, 10, len(res.BindingList)) assert.Equal(t, res.BindingList[0], WorkerBindingListItem{ Name: "MY_KV", @@ -191,6 +200,15 @@ func TestListWorkerBindings_Wfp(t *testing.T) { }, }) assert.Equal(t, WorkerD1DataseBindingType, res.BindingList[8].Binding.Type()) + + assert.Equal(t, res.BindingList[9], WorkerBindingListItem{ + Name: "MY_HYPERDRIVE", + Binding: WorkerHyperdriveBinding{ + Binding: "MY_HYPERDRIVE", + ConfigID: "aaf4609248cc493cbc8d3e446e38fdfa", + }, + }) + assert.Equal(t, WorkerHyperdriveBindingType, res.BindingList[9].Binding.Type()) } func ExampleUnsafeBinding() { diff --git a/workers_test.go b/workers_test.go index 6175440a130..ef0e09987a6 100644 --- a/workers_test.go +++ b/workers_test.go @@ -145,6 +145,11 @@ const ( "name": "MY_DATABASE", "type": "d1", "database_id": "cef5331f-e5c7-4c8a-a415-7908ae45f92a" + }, + { + "name": "MY_HYPERDRIVE", + "type": "hyperdrive", + "id": "aaf4609248cc493cbc8d3e446e38fdfa" } ], "success": true, diff --git a/zone.go b/zone.go index de92880a157..714c0edc8bf 100644 --- a/zone.go +++ b/zone.go @@ -146,11 +146,12 @@ type ZoneRatePlanResponse struct { // ZoneSetting contains settings for a zone. type ZoneSetting struct { - ID string `json:"id"` - Editable bool `json:"editable"` - ModifiedOn string `json:"modified_on,omitempty"` - Value interface{} `json:"value"` - TimeRemaining int `json:"time_remaining"` + ID string `json:"id"` + Editable bool `json:"editable"` + ModifiedOn string `json:"modified_on,omitempty"` + Value interface{} `json:"value"` + TimeRemaining int `json:"time_remaining"` + NextScheduledScan string `json:"next_scheduled_scan,omitempty"` } // ZoneSettingResponse represents the response from the Zone Setting endpoint. @@ -175,7 +176,6 @@ type ZoneSSLSetting struct { } // ZoneSSLSettingResponse represents the response from the Zone SSL Setting -// endpoint. type ZoneSSLSettingResponse struct { Response Result ZoneSSLSetting `json:"result"` diff --git a/zone_test.go b/zone_test.go index d8b94cd1c65..d5a28fa4f28 100644 --- a/zone_test.go +++ b/zone_test.go @@ -1511,6 +1511,65 @@ func TestUpdateZoneSSLSettings(t *testing.T) { } } +func TestUpdateZoneAutomaticSSLModeSetting(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") + // JSON data from: https://api.cloudflare.com/#zone-settings-properties + _, _ = fmt.Fprintf(w, `{ + "result": { + "id": "ssl_tls_configuration_mode", + "value": "auto", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z", + "next_scheduled_scan": "2014-01-01T05:20:00.12345Z" + } + }`) + } + mux.HandleFunc("/zones/foo/settings/ssl_automatic_mode", handler) + s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{ + Name: "ssl_automatic_mode", + Value: "auto", + }) + + if assert.NoError(t, err) { + assert.Equal(t, s.ID, "ssl_tls_configuration_mode") + assert.Equal(t, s.Value, "auto") + assert.Equal(t, s.Editable, true) + assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") + assert.Equal(t, s.NextScheduledScan, "2014-01-01T05:20:00.12345Z") + } +} + +func TestGetZoneAutomaticSSLModeSetting(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_tls_configuration_mode", + "value": "custom", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z", + "next_scheduled_scan": "2024-01-01T05:20:00.12345Z" + } + }`) + } + mux.HandleFunc("/zones/foo/settings/ssl_automatic_mode", handler) + s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl_automatic_mode"}) + if assert.NoError(t, err) { + assert.Equal(t, s.ID, "ssl_tls_configuration_mode") + assert.Equal(t, s.Value, "custom") + assert.Equal(t, s.Editable, true) + assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") + assert.Equal(t, s.NextScheduledScan, "2024-01-01T05:20:00.12345Z") + } +} + func TestGetZoneSetting(t *testing.T) { setup() defer teardown()