Skip to content

Conversation

@ang-cloudflare
Copy link
Contributor

@ang-cloudflare ang-cloudflare commented Feb 2, 2026

Summary

Fixes the cloudflare_authenticated_origin_pulls resource which was completely broken due to API response format mismatch.

Problem

Primary Issue: Array Response Not Handled

The API endpoint PUT /zones/{zone_id}/origin_tls_client_auth/hostnames returns an array of all hostname associations for the zone, not just the single hostname being created/updated:

{
  "result": [
    {"hostname": "example.com", "cert_id": "abc-123", "enabled": true, ...},
    {"hostname": "api.example.com", "cert_id": "def-456", "enabled": true, ...}
  ],
  "success": true
}

The auto-generated provider code expected a single object:

{
  "result": {"hostname": "example.com", "cert_id": "abc-123", ...}
}

This caused Create/Update operations to silently fail - the API call succeeded but the response couldn't be parsed, leaving the resource state empty or incorrect.

Secondary Issues

  1. Delete failed with "Certificate ID required" - The API requires cert_id in the delete payload, but it wasn't being sent
  2. State drift on private_key - This is a write-only field (sent to API, never returned), causing Terraform to detect drift on every refresh

Solution

1. Array Response Handling (custom.go)

Created custom types to properly handle the array response:

// AuthenticatedOriginPullsArrayResultEnvelope handles the array response from the API
type AuthenticatedOriginPullsArrayResultEnvelope struct {
    Result []AuthenticatedOriginPullsModel `json:"result"`
}

// FindByHostname searches the array for a specific hostname
func (e *AuthenticatedOriginPullsArrayResultEnvelope) FindByHostname(hostname string) (*AuthenticatedOriginPullsModel, error) {
    for i := range e.Result {
        if e.Result[i].Hostname.ValueString() == hostname {
            return &e.Result[i], nil
        }
    }
    return nil, fmt.Errorf("hostname %q not found in response", hostname)
}

2. Fixed CRUD Operations (resource.go)

Create/Update:

// Parse array response
env := AuthenticatedOriginPullsArrayResultEnvelope{}
err = apijson.UnmarshalComputed(bytes, &env)

// Find matching hostname from the array
result, err := env.FindByHostname(targetHostname)

// Copy computed fields to state
data.CERTID = result.CERTID
data.Enabled = result.Enabled
// ... etc

Delete:

// Include cert_id as required by API
deletePayload := map[string]interface{}{
    "config": []map[string]interface{}{
        {
            "hostname": hostname,
            "cert_id":  data.CERTID.ValueString(), // Required!
            "enabled":  nil, // null voids the association
        },
    },
}

Read:

// PrivateKey is write-only, set to null to prevent drift
data.PrivateKey = types.StringNull()

3. Comprehensive Lifecycle Test (resource_test.go)

Rewrote tests to cover the full lifecycle:

  • Step 1: Create hostname association with enabled=true
  • Step 2: Update to enabled=false
  • Step 3: Import and verify state matches

API Documentation Verification

Verified implementation against Cloudflare API docs:

API Behavior Implementation Status
PUT returns array of all hostname associations AuthenticatedOriginPullsArrayResultEnvelope with FindByHostname()
GET returns single hostname association Uses AuthenticatedOriginPullsResultEnvelope (single object)
Delete uses enabled: null to void association Sends enabled: nil in payload
cert_id required in request Included in Create/Update/Delete payloads
private_key is write-only Set to types.StringNull() after API calls

Files Changed

File Purpose
custom.go New file - Array envelope type and FindByHostname() helper
resource.go Fixed Create/Update/Delete/Read methods
resource_test.go Rewrote with comprehensive lifecycle test
testdata/*.tf Removed outdated test configs

Cherry-picked Dependencies

This PR includes cherry-picked certificate normalization fixes required for the test to pass (the test creates a certificate first, then associates it with a hostname):

Test Results

=== RUN   TestAccAuthenticatedOriginPulls_FullLifecycle
--- PASS: TestAccAuthenticatedOriginPulls_FullLifecycle (8.21s)
PASS
ok  	github.com/cloudflare/terraform-provider-cloudflare/internal/services/authenticated_origin_pulls	9.633s

JIRA

SECENG-12970

@ang-cloudflare ang-cloudflare requested a review from a team as a code owner February 2, 2026 12:39
@vaishakdinesh
Copy link
Member

The API endpoint PUT /zones/{zone_id}/origin_tls_client_auth/hostnames returns an array of all hostname associations for the zone, not just the single hostname being created/updated:

Is the openapi definition correct?

@ang-cloudflare
Copy link
Contributor Author

updated

Yes, the OpenAPI spec in api-docs correctly defines the response as hostname_aop_response_collection with result: type: array

…full lifecycle

SECENG-12970

The API endpoint PUT /zones/{zone_id}/origin_tls_client_auth/hostnames
returns an array of hostname associations, but the auto-generated code
expected a single object. This caused Create/Update operations to fail
silently.

Changes for authenticated_origin_pulls:
- Add custom.go with array envelope type and FindByHostname() helper
- Fix Create/Update methods to parse array response and find matching hostname
- Fix Delete method to include cert_id in payload (API requirement)
- Fix Read method to set PrivateKey to null (write-only field)
- Rewrite resource_test.go with comprehensive lifecycle test
- Remove outdated testdata files

Also includes cherry-picked fixes:
- Certificate normalization from PR cloudflare#6710 (authenticated_origin_pulls_certificate)
- Certificate normalization from PR cloudflare#6703 (authenticated_origin_pulls_hostname_certificate)

Test: PASS TestAccAuthenticatedOriginPulls_FullLifecycle (8.21s)
- Add config length validation requiring exactly one hostname association
- Move authenticated_origin_pulls_certificate test config to testdata file
- Add updated_at to ImportStateVerifyIgnore list
Copy link
Contributor

@musa-cf musa-cf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for providing a great description!

@vaishakdinesh vaishakdinesh merged commit 277d07e into cloudflare:next Feb 3, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants