diff --git a/internal/controller/account_controller_test.go b/internal/controller/account_controller_test.go index 0c5b1315..939510ea 100644 --- a/internal/controller/account_controller_test.go +++ b/internal/controller/account_controller_test.go @@ -32,7 +32,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cloudflare "github.com/cloudflare/cloudflare-go/v7" cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" networkingv1 "k8s.io/api/networking/v1" ) @@ -47,7 +46,7 @@ func NewTestScheme() *runtime.Scheme { } var ( - cloudflareAPI *cloudflare.Client + cloudflareAPI *cloudflareClient cloudflareAPIToken string ) diff --git a/internal/controller/cloudflare_api.go b/internal/controller/cloudflare_api.go index be4136a3..c848cda3 100644 --- a/internal/controller/cloudflare_api.go +++ b/internal/controller/cloudflare_api.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" - cloudflare "github.com/cloudflare/cloudflare-go/v7" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,11 +28,11 @@ import ( cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" ) -func cloudflareAPIFromZone(ctx context.Context, kubeClient client.Client, zone *cloudflareoperatoriov1.Zone) (*cloudflare.Client, error) { +func cloudflareAPIFromZone(ctx context.Context, kubeClient client.Client, zone *cloudflareoperatoriov1.Zone) (*cloudflareClient, error) { return cloudflareAPIForAccountName(ctx, kubeClient, zone.Spec.AccountRef.Name) } -func cloudflareAPIFromDNSRecord(ctx context.Context, kubeClient client.Client, dnsRecord *cloudflareoperatoriov1.DNSRecord, zone *cloudflareoperatoriov1.Zone) (*cloudflare.Client, error) { +func cloudflareAPIFromDNSRecord(ctx context.Context, kubeClient client.Client, dnsRecord *cloudflareoperatoriov1.DNSRecord, zone *cloudflareoperatoriov1.Zone) (*cloudflareClient, error) { accountName := dnsRecord.Spec.AccountRef.Name if zone != nil && zone.Spec.AccountRef.Name != "" { if accountName != "" && accountName != zone.Spec.AccountRef.Name { @@ -45,7 +44,7 @@ func cloudflareAPIFromDNSRecord(ctx context.Context, kubeClient client.Client, d return cloudflareAPIForAccountName(ctx, kubeClient, accountName) } -func cloudflareAPIForAccountName(ctx context.Context, kubeClient client.Client, accountName string) (*cloudflare.Client, error) { +func cloudflareAPIForAccountName(ctx context.Context, kubeClient client.Client, accountName string) (*cloudflareClient, error) { account, err := accountForName(ctx, kubeClient, accountName) if err != nil { return nil, err diff --git a/internal/controller/cloudflare_dns.go b/internal/controller/cloudflare_dns.go index 8232ba2e..6bea8eeb 100644 --- a/internal/controller/cloudflare_dns.go +++ b/internal/controller/cloudflare_dns.go @@ -23,7 +23,6 @@ import ( "fmt" "net/http" - cloudflare "github.com/cloudflare/cloudflare-go/v7" "github.com/cloudflare/cloudflare-go/v7/dns" "github.com/cloudflare/cloudflare-go/v7/option" "github.com/cloudflare/cloudflare-go/v7/zones" @@ -31,15 +30,29 @@ import ( cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" ) -func newCloudflareClient(token string) *cloudflare.Client { - return cloudflare.NewClient(option.WithAPIToken(token)) +type cloudflareClient struct { + DNS *dns.DNSService + Zones *zones.ZoneService } -func cloudflareZoneIDByName(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneName string) (string, error) { - pager := cloudflareAPI.Zones.ListAutoPaging(ctx, zones.ZoneListParams{ - Name: cloudflare.String(zoneName), - PerPage: cloudflare.Float(50), - }) +func newCloudflareClient(token string, opts ...option.RequestOption) *cloudflareClient { + opts = append([]option.RequestOption{ + option.WithEnvironmentProduction(), + option.WithAPIToken(token), + }, opts...) + return &cloudflareClient{ + DNS: dns.NewDNSService(opts...), + Zones: zones.NewZoneService(opts...), + } +} + +func cloudflareZoneIDByName(ctx context.Context, cloudflareAPI *cloudflareClient, zoneName string) (string, error) { + params := zones.ZoneListParams{} + params.Name.Value = zoneName + params.Name.Present = true + params.PerPage.Value = 50 + params.PerPage.Present = true + pager := cloudflareAPI.Zones.ListAutoPaging(ctx, params) for pager.Next() { zone := pager.Current() if zone.Name == zoneName { @@ -53,20 +66,23 @@ func cloudflareZoneIDByName(ctx context.Context, cloudflareAPI *cloudflare.Clien return "", errors.New("zone could not be found") } -func getCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneID, recordID string) (dns.RecordResponse, error) { - record, err := cloudflareAPI.DNS.Records.Get(ctx, recordID, dns.RecordGetParams{ - ZoneID: cloudflare.String(zoneID), - }) +func getCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflareClient, zoneID, recordID string) (dns.RecordResponse, error) { + params := dns.RecordGetParams{} + params.ZoneID.Value = zoneID + params.ZoneID.Present = true + record, err := cloudflareAPI.DNS.Records.Get(ctx, recordID, params) if err != nil { return dns.RecordResponse{}, err } return *record, nil } -func listCloudflareDNSRecords(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneID string, params dns.RecordListParams) ([]dns.RecordResponse, error) { - params.ZoneID = cloudflare.String(zoneID) +func listCloudflareDNSRecords(ctx context.Context, cloudflareAPI *cloudflareClient, zoneID string, params dns.RecordListParams) ([]dns.RecordResponse, error) { + params.ZoneID.Value = zoneID + params.ZoneID.Present = true if !params.PerPage.Present { - params.PerPage = cloudflare.Float(1000) + params.PerPage.Value = 1000 + params.PerPage.Present = true } var records []dns.RecordResponse @@ -80,42 +96,43 @@ func listCloudflareDNSRecords(ctx context.Context, cloudflareAPI *cloudflare.Cli return records, nil } -func createCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneID string, desiredRecord cloudflareoperatoriov1.DNSRecordSpec) (dns.RecordResponse, error) { +func createCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflareClient, zoneID string, desiredRecord cloudflareoperatoriov1.DNSRecordSpec) (dns.RecordResponse, error) { body, err := newCloudflareDNSRecordBody(desiredRecord) if err != nil { return dns.RecordResponse{}, err } - record, err := cloudflareAPI.DNS.Records.New(ctx, dns.RecordNewParams{ - ZoneID: cloudflare.String(zoneID), - Body: body, - }) + params := dns.RecordNewParams{Body: body} + params.ZoneID.Value = zoneID + params.ZoneID.Present = true + record, err := cloudflareAPI.DNS.Records.New(ctx, params) if err != nil { return dns.RecordResponse{}, err } return *record, nil } -func editCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneID, recordID string, desiredRecord cloudflareoperatoriov1.DNSRecordSpec) error { +func editCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflareClient, zoneID, recordID string, desiredRecord cloudflareoperatoriov1.DNSRecordSpec) error { body, err := editCloudflareDNSRecordBody(desiredRecord) if err != nil { return err } - _, err = cloudflareAPI.DNS.Records.Edit(ctx, recordID, dns.RecordEditParams{ - ZoneID: cloudflare.String(zoneID), - Body: body, - }) + params := dns.RecordEditParams{Body: body} + params.ZoneID.Value = zoneID + params.ZoneID.Present = true + _, err = cloudflareAPI.DNS.Records.Edit(ctx, recordID, params) return err } -func deleteCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneID, recordID string) error { +func deleteCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflareClient, zoneID, recordID string) error { if recordID == "" { return nil } - _, err := cloudflareAPI.DNS.Records.Delete(ctx, recordID, dns.RecordDeleteParams{ - ZoneID: cloudflare.String(zoneID), - }) + params := dns.RecordDeleteParams{} + params.ZoneID.Value = zoneID + params.ZoneID.Present = true + _, err := cloudflareAPI.DNS.Records.Delete(ctx, recordID, params) return err } @@ -125,21 +142,20 @@ func newCloudflareDNSRecordBody(desiredRecord cloudflareoperatoriov1.DNSRecordSp return dns.RecordNewParamsBody{}, err } - body := dns.RecordNewParamsBody{ - Name: cloudflare.String(desiredRecord.Name), - TTL: cloudflare.F(dns.TTL(normalizedTTL(desiredRecord.TTL))), - Type: cloudflare.F(dns.RecordNewParamsBodyType(desiredRecord.Type)), - Proxied: cloudflare.Bool(proxiedEnabled(desiredRecord.Proxied)), - Comment: cloudflare.String(desiredRecord.Comment), - } + body := dns.RecordNewParamsBody{} + body.Name.Value, body.Name.Present = desiredRecord.Name, true + body.TTL.Value, body.TTL.Present = dns.TTL(normalizedTTL(desiredRecord.TTL)), true + body.Type.Value, body.Type.Present = dns.RecordNewParamsBodyType(desiredRecord.Type), true + body.Proxied.Value, body.Proxied.Present = proxiedEnabled(desiredRecord.Proxied), true + body.Comment.Value, body.Comment.Present = desiredRecord.Comment, true if desiredRecord.Content != "" || data == nil { - body.Content = cloudflare.String(desiredRecord.Content) + body.Content.Value, body.Content.Present = desiredRecord.Content, true } if desiredRecord.Priority != nil { - body.Priority = cloudflare.Float(float64(*desiredRecord.Priority)) + body.Priority.Value, body.Priority.Present = float64(*desiredRecord.Priority), true } if data != nil { - body.Data = cloudflare.F[interface{}](data) + body.Data.Value, body.Data.Present = data, true } return body, nil } @@ -150,21 +166,20 @@ func editCloudflareDNSRecordBody(desiredRecord cloudflareoperatoriov1.DNSRecordS return dns.RecordEditParamsBody{}, err } - body := dns.RecordEditParamsBody{ - Name: cloudflare.String(desiredRecord.Name), - TTL: cloudflare.F(dns.TTL(normalizedTTL(desiredRecord.TTL))), - Type: cloudflare.F(dns.RecordEditParamsBodyType(desiredRecord.Type)), - Proxied: cloudflare.Bool(proxiedEnabled(desiredRecord.Proxied)), - Comment: cloudflare.String(desiredRecord.Comment), - } + body := dns.RecordEditParamsBody{} + body.Name.Value, body.Name.Present = desiredRecord.Name, true + body.TTL.Value, body.TTL.Present = dns.TTL(normalizedTTL(desiredRecord.TTL)), true + body.Type.Value, body.Type.Present = dns.RecordEditParamsBodyType(desiredRecord.Type), true + body.Proxied.Value, body.Proxied.Present = proxiedEnabled(desiredRecord.Proxied), true + body.Comment.Value, body.Comment.Present = desiredRecord.Comment, true if desiredRecord.Content != "" || data == nil { - body.Content = cloudflare.String(desiredRecord.Content) + body.Content.Value, body.Content.Present = desiredRecord.Content, true } if desiredRecord.Priority != nil { - body.Priority = cloudflare.Float(float64(*desiredRecord.Priority)) + body.Priority.Value, body.Priority.Present = float64(*desiredRecord.Priority), true } if data != nil { - body.Data = cloudflare.F[interface{}](data) + body.Data.Value, body.Data.Present = data, true } return body, nil } diff --git a/internal/controller/cloudflare_dns_test.go b/internal/controller/cloudflare_dns_test.go new file mode 100644 index 00000000..52235c04 --- /dev/null +++ b/internal/controller/cloudflare_dns_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2025 containeroo + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/cloudflare/cloudflare-go/v7/option" + . "github.com/onsi/gomega" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestNewCloudflareClientUsesProductionEnvironment(t *testing.T) { + g := NewWithT(t) + + httpClient := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + g.Expect(req.URL.Scheme).To(Equal("https")) + g.Expect(req.URL.Host).To(Equal("api.cloudflare.com")) + g.Expect(req.URL.Path).To(Equal("/client/v4/zones")) + g.Expect(req.Header.Get("Authorization")).To(Equal("Bearer token")) + + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{ + "result": [{"id": "zone-id", "name": "example.com"}], + "result_info": {"page": 1, "per_page": 50, "count": 1, "total_count": 1, "total_pages": 1}, + "success": true, + "errors": [], + "messages": [] + }`)), + Request: req, + }, nil + })} + + client := newCloudflareClient("token", option.WithHTTPClient(httpClient)) + zoneID, err := cloudflareZoneIDByName(context.Background(), client, "example.com") + + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(zoneID).To(Equal("zone-id")) +} diff --git a/internal/controller/dnsrecord_controller.go b/internal/controller/dnsrecord_controller.go index e2e57c86..16675b6f 100644 --- a/internal/controller/dnsrecord_controller.go +++ b/internal/controller/dnsrecord_controller.go @@ -40,7 +40,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - cloudflare "github.com/cloudflare/cloudflare-go/v7" "github.com/cloudflare/cloudflare-go/v7/dns" cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" intconditions "github.com/containeroo/cloudflare-operator/internal/conditions" @@ -203,12 +202,13 @@ func (r *DNSRecordReconciler) reconcileDNSRecord(ctx context.Context, dnsrecord return ctrl.Result{RequeueAfter: r.RetryInterval}, nil } } else { - cloudflareExistingRecord, err := listCloudflareDNSRecords(ctx, cloudflareAPI, zone.Status.ID, dns.RecordListParams{ - Type: cloudflare.F(dns.RecordListParamsType(dnsrecord.Spec.Type)), - Name: cloudflare.F(dns.RecordListParamsName{ - Exact: cloudflare.String(dnsrecord.Spec.Name), - }), - }) + params := dns.RecordListParams{} + params.Type.Value = dns.RecordListParamsType(dnsrecord.Spec.Type) + params.Type.Present = true + params.Name.Value.Exact.Value = dnsrecord.Spec.Name + params.Name.Value.Exact.Present = true + params.Name.Present = true + cloudflareExistingRecord, err := listCloudflareDNSRecords(ctx, cloudflareAPI, zone.Status.ID, params) if err != nil { intconditions.MarkFalse(dnsrecord, err) return ctrl.Result{RequeueAfter: r.RetryInterval}, nil diff --git a/internal/controller/zone_controller.go b/internal/controller/zone_controller.go index c7dc813f..49285df2 100644 --- a/internal/controller/zone_controller.go +++ b/internal/controller/zone_controller.go @@ -24,7 +24,6 @@ import ( "strings" "time" - cloudflare "github.com/cloudflare/cloudflare-go/v7" "github.com/cloudflare/cloudflare-go/v7/dns" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -165,7 +164,7 @@ func (r *ZoneReconciler) reconcileZone(ctx context.Context, zone *cloudflareoper } // handlePrune deletes DNS records that are not managed by the operator if enabled -func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudflare.Client, zone *cloudflareoperatoriov1.Zone) error { +func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudflareClient, zone *cloudflareoperatoriov1.Zone) error { log := ctrl.LoggerFrom(ctx) zones := &cloudflareoperatoriov1.ZoneList{}