diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go index d184f6d5..fe5b3c40 100644 --- a/api/v1/groupversion_info.go +++ b/api/v1/groupversion_info.go @@ -20,16 +20,30 @@ limitations under the License. package v1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" ) +type schemeBuilder struct { + GroupVersion schema.GroupVersion + runtime.SchemeBuilder +} + +func (b *schemeBuilder) Register(objects ...runtime.Object) { + b.SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(b.GroupVersion, objects...) + metav1.AddToGroupVersion(s, b.GroupVersion) + return nil + }) +} + var ( // GroupVersion is group version used to register these objects GroupVersion = schema.GroupVersion{Group: "cloudflare-operator.io", Version: "v1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + SchemeBuilder = &schemeBuilder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 4b21edae..0abb7896 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1 import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/go.mod b/go.mod index e06933c7..bc75f28c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/containeroo/cloudflare-operator go 1.26.0 require ( - github.com/cloudflare/cloudflare-go v0.117.0 + github.com/cloudflare/cloudflare-go/v7 v7.4.0 github.com/fluxcd/pkg/runtime v0.108.0 github.com/itchyny/gojq v0.12.19 github.com/onsi/ginkgo/v2 v2.29.0 @@ -33,10 +33,8 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/uuid v1.6.0 // indirect github.com/itchyny/timefmt-go v0.1.8 // indirect @@ -52,6 +50,10 @@ require ( github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/go.sum b/go.sum index 91de0ea7..36d0fe42 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/cloudflare-go v0.117.0 h1:y00E0XCvxuZGplL+gkoMRIhWpfNqIgyBFS6UUWC4s0c= -github.com/cloudflare/cloudflare-go v0.117.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= +github.com/cloudflare/cloudflare-go/v7 v7.4.0 h1:JdTxzeXcAhtJ9rUkNISK4ABA55pZP8HLxx6XsPSA7dU= +github.com/cloudflare/cloudflare-go/v7 v7.4.0/go.mod h1:9zcoIAtu6cmcoPszCNISvqYMXs8wObtVGXE1qGFMrNU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -44,17 +44,12 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -121,10 +116,12 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= @@ -159,7 +156,6 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= diff --git a/internal/controller/account_controller.go b/internal/controller/account_controller.go index affddf38..f28ef4ce 100644 --- a/internal/controller/account_controller.go +++ b/internal/controller/account_controller.go @@ -21,7 +21,6 @@ import ( "errors" "time" - "github.com/cloudflare/cloudflare-go" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -123,10 +122,7 @@ func (r *AccountReconciler) reconcileAccount(ctx context.Context, account *cloud return ctrl.Result{RequeueAfter: r.RetryInterval}, nil } - if _, err := cloudflare.NewWithAPIToken(cloudflareAPIToken); err != nil { - intconditions.MarkFalse(account, err) - return ctrl.Result{}, err - } + _ = newCloudflareClient(cloudflareAPIToken) intconditions.MarkTrue(account, "Account is ready") diff --git a/internal/controller/account_controller_test.go b/internal/controller/account_controller_test.go index 032fad24..0c5b1315 100644 --- a/internal/controller/account_controller_test.go +++ b/internal/controller/account_controller_test.go @@ -32,7 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/cloudflare/cloudflare-go" + cloudflare "github.com/cloudflare/cloudflare-go/v7" cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" networkingv1 "k8s.io/api/networking/v1" ) @@ -46,28 +46,40 @@ func NewTestScheme() *runtime.Scheme { return s } -var cloudflareAPI cloudflare.API +var ( + cloudflareAPI *cloudflare.Client + cloudflareAPIToken string +) + +const ( + testAccountName = "account" + testContentAnnotation = "cloudflare-operator.io/content" + testDefaultNamespace = "default" + testDNSRecordHost = "dnstest.containeroo-test.org" + testIPv4Address = "1.1.1.1" + testAlternateIPv4Address = "2.2.2.2" + testRecordTypeTXT = "TXT" + testSecretName = "secret" + testWildcardDNSRecordName = "wildcard-containeroo-test-org" + testWildcardHost = "*.containeroo-test.org" +) func initTestCloudflareAPI(t *testing.T) { t.Helper() - if cloudflareAPI.APIToken == os.Getenv("CF_API_TOKEN") && cloudflareAPI.APIToken != "" { + if cloudflareAPI != nil && cloudflareAPIToken == os.Getenv("CF_API_TOKEN") { return } - api, err := cloudflare.NewWithAPIToken(os.Getenv("CF_API_TOKEN")) - if err != nil { - t.Fatalf("failed to initialize test Cloudflare API: %v", err) - } - - cloudflareAPI = *api + cloudflareAPIToken = os.Getenv("CF_API_TOKEN") + cloudflareAPI = newCloudflareClient(cloudflareAPIToken) } func NewTestAccountObjects() (*corev1.Secret, *cloudflareoperatoriov1.Account) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", + Name: testSecretName, + Namespace: testDefaultNamespace, }, Data: map[string][]byte{ "apiToken": []byte(os.Getenv("CF_API_TOKEN")), @@ -76,7 +88,7 @@ func NewTestAccountObjects() (*corev1.Secret, *cloudflareoperatoriov1.Account) { account := &cloudflareoperatoriov1.Account{ ObjectMeta: metav1.ObjectMeta{ - Name: "account", + Name: testAccountName, }, Spec: cloudflareoperatoriov1.AccountSpec{ ApiToken: cloudflareoperatoriov1.AccountSpecApiToken{ @@ -118,13 +130,13 @@ func TestAccountReconciler_reconcileAccount(t *testing.T) { account := &cloudflareoperatoriov1.Account{ ObjectMeta: metav1.ObjectMeta{ - Name: "account", + Name: testAccountName, }, Spec: cloudflareoperatoriov1.AccountSpec{ ApiToken: cloudflareoperatoriov1.AccountSpecApiToken{ SecretRef: corev1.SecretReference{ - Name: "secret", - Namespace: "default", + Name: testSecretName, + Namespace: testDefaultNamespace, }, }, }, @@ -150,8 +162,8 @@ func TestAccountReconciler_reconcileAccount(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", + Name: testSecretName, + Namespace: testDefaultNamespace, }, Data: map[string][]byte{ "invalid": []byte("invalid"), @@ -160,13 +172,13 @@ func TestAccountReconciler_reconcileAccount(t *testing.T) { account := &cloudflareoperatoriov1.Account{ ObjectMeta: metav1.ObjectMeta{ - Name: "account", + Name: testAccountName, }, Spec: cloudflareoperatoriov1.AccountSpec{ ApiToken: cloudflareoperatoriov1.AccountSpecApiToken{ SecretRef: corev1.SecretReference{ - Name: "secret", - Namespace: "default", + Name: testSecretName, + Namespace: testDefaultNamespace, }, }, }, @@ -229,6 +241,6 @@ func TestCloudflareAPIForAccountName(t *testing.T) { api, err := cloudflareAPIForAccountName(context.TODO(), kubeClient, otherAccount.Name) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(api.APIToken).To(Equal(string(otherSecret.Data["apiToken"]))) + g.Expect(api).ToNot(BeNil()) }) } diff --git a/internal/controller/cloudflare_api.go b/internal/controller/cloudflare_api.go index 6e86a291..be4136a3 100644 --- a/internal/controller/cloudflare_api.go +++ b/internal/controller/cloudflare_api.go @@ -21,7 +21,7 @@ import ( "errors" "fmt" - "github.com/cloudflare/cloudflare-go" + 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 +29,11 @@ import ( cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" ) -func cloudflareAPIFromZone(ctx context.Context, kubeClient client.Client, zone *cloudflareoperatoriov1.Zone) (*cloudflare.API, error) { +func cloudflareAPIFromZone(ctx context.Context, kubeClient client.Client, zone *cloudflareoperatoriov1.Zone) (*cloudflare.Client, error) { return cloudflareAPIForAccountName(ctx, kubeClient, zone.Spec.AccountRef.Name) } -func cloudflareAPIFromDNSRecord(ctx context.Context, kubeClient client.Client, dnsRecord *cloudflareoperatoriov1.DNSRecord, zone *cloudflareoperatoriov1.Zone) (*cloudflare.API, error) { +func cloudflareAPIFromDNSRecord(ctx context.Context, kubeClient client.Client, dnsRecord *cloudflareoperatoriov1.DNSRecord, zone *cloudflareoperatoriov1.Zone) (*cloudflare.Client, error) { accountName := dnsRecord.Spec.AccountRef.Name if zone != nil && zone.Spec.AccountRef.Name != "" { if accountName != "" && accountName != zone.Spec.AccountRef.Name { @@ -45,7 +45,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.API, error) { +func cloudflareAPIForAccountName(ctx context.Context, kubeClient client.Client, accountName string) (*cloudflare.Client, error) { account, err := accountForName(ctx, kubeClient, accountName) if err != nil { return nil, err @@ -56,12 +56,7 @@ func cloudflareAPIForAccountName(ctx context.Context, kubeClient client.Client, return nil, err } - cloudflareAPI, err := cloudflare.NewWithAPIToken(token) - if err != nil { - return nil, fmt.Errorf("failed to create Cloudflare API client for account %q: %w", account.Name, err) - } - - return cloudflareAPI, nil + return newCloudflareClient(token), nil } func accountForName(ctx context.Context, kubeClient client.Client, accountName string) (*cloudflareoperatoriov1.Account, error) { diff --git a/internal/controller/cloudflare_dns.go b/internal/controller/cloudflare_dns.go new file mode 100644 index 00000000..8232ba2e --- /dev/null +++ b/internal/controller/cloudflare_dns.go @@ -0,0 +1,194 @@ +/* +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" + "encoding/json" + "errors" + "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" + + cloudflareoperatoriov1 "github.com/containeroo/cloudflare-operator/api/v1" +) + +func newCloudflareClient(token string) *cloudflare.Client { + return cloudflare.NewClient(option.WithAPIToken(token)) +} + +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), + }) + for pager.Next() { + zone := pager.Current() + if zone.Name == zoneName { + return zone.ID, nil + } + } + if err := pager.Err(); err != nil { + return "", err + } + + 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), + }) + 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) + if !params.PerPage.Present { + params.PerPage = cloudflare.Float(1000) + } + + var records []dns.RecordResponse + pager := cloudflareAPI.DNS.Records.ListAutoPaging(ctx, params) + for pager.Next() { + records = append(records, pager.Current()) + } + if err := pager.Err(); err != nil { + return nil, err + } + return records, nil +} + +func createCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflare.Client, 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, + }) + 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 { + body, err := editCloudflareDNSRecordBody(desiredRecord) + if err != nil { + return err + } + + _, err = cloudflareAPI.DNS.Records.Edit(ctx, recordID, dns.RecordEditParams{ + ZoneID: cloudflare.String(zoneID), + Body: body, + }) + return err +} + +func deleteCloudflareDNSRecord(ctx context.Context, cloudflareAPI *cloudflare.Client, zoneID, recordID string) error { + if recordID == "" { + return nil + } + _, err := cloudflareAPI.DNS.Records.Delete(ctx, recordID, dns.RecordDeleteParams{ + ZoneID: cloudflare.String(zoneID), + }) + return err +} + +func newCloudflareDNSRecordBody(desiredRecord cloudflareoperatoriov1.DNSRecordSpec) (dns.RecordNewParamsBody, error) { + data, err := cloudflareDNSRecordData(desiredRecord) + if err != nil { + 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), + } + if desiredRecord.Content != "" || data == nil { + body.Content = cloudflare.String(desiredRecord.Content) + } + if desiredRecord.Priority != nil { + body.Priority = cloudflare.Float(float64(*desiredRecord.Priority)) + } + if data != nil { + body.Data = cloudflare.F[interface{}](data) + } + return body, nil +} + +func editCloudflareDNSRecordBody(desiredRecord cloudflareoperatoriov1.DNSRecordSpec) (dns.RecordEditParamsBody, error) { + data, err := cloudflareDNSRecordData(desiredRecord) + if err != nil { + 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), + } + if desiredRecord.Content != "" || data == nil { + body.Content = cloudflare.String(desiredRecord.Content) + } + if desiredRecord.Priority != nil { + body.Priority = cloudflare.Float(float64(*desiredRecord.Priority)) + } + if data != nil { + body.Data = cloudflare.F[interface{}](data) + } + return body, nil +} + +func cloudflareDNSRecordData(desiredRecord cloudflareoperatoriov1.DNSRecordSpec) (any, error) { + if desiredRecord.Data == nil { + return nil, nil + } + + var data any + if err := json.Unmarshal(desiredRecord.Data.Raw, &data); err != nil { + return nil, fmt.Errorf("failed to parse DNS record data: %w", err) + } + return data, nil +} + +func normalizedTTL(ttl int) float64 { + if ttl == 0 { + return 1 + } + return float64(ttl) +} + +func isCloudflareDNSRecordNotFound(err error) bool { + var apiErr *dns.Error + return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound +} diff --git a/internal/controller/dnsrecord_controller.go b/internal/controller/dnsrecord_controller.go index 1a02ae51..e2e57c86 100644 --- a/internal/controller/dnsrecord_controller.go +++ b/internal/controller/dnsrecord_controller.go @@ -40,7 +40,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/cloudflare/cloudflare-go" + 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" interrors "github.com/containeroo/cloudflare-operator/internal/errors" @@ -194,17 +195,19 @@ func (r *DNSRecordReconciler) reconcileDNSRecord(ctx context.Context, dnsrecord return ctrl.Result{RequeueAfter: r.RetryInterval}, errWaitForZone } - var existingRecord cloudflare.DNSRecord + var existingRecord dns.RecordResponse if dnsrecord.Status.RecordID != "" { - existingRecord, err = cloudflareAPI.GetDNSRecord(ctx, cloudflare.ZoneIdentifier(zone.Status.ID), dnsrecord.Status.RecordID) + existingRecord, err = getCloudflareDNSRecord(ctx, cloudflareAPI, zone.Status.ID, dnsrecord.Status.RecordID) if err != nil { intconditions.MarkFalse(dnsrecord, err) return ctrl.Result{RequeueAfter: r.RetryInterval}, nil } } else { - cloudflareExistingRecord, _, err := cloudflareAPI.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zone.Status.ID), cloudflare.ListDNSRecordsParams{ - Type: dnsrecord.Spec.Type, - Name: dnsrecord.Spec.Name, + 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), + }), }) if err != nil { intconditions.MarkFalse(dnsrecord, err) @@ -224,35 +227,14 @@ func (r *DNSRecordReconciler) reconcileDNSRecord(ctx context.Context, dnsrecord } if existingRecord.ID == "" { - proxied := proxiedPtr(proxiedEnabled(desiredRecord.Proxied)) - newDNSRecord, err := cloudflareAPI.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone.Status.ID), cloudflare.CreateDNSRecordParams{ - Name: desiredRecord.Name, - Type: desiredRecord.Type, - Content: desiredRecord.Content, - TTL: desiredRecord.TTL, - Proxied: proxied, - Priority: desiredRecord.Priority, - Data: desiredRecord.Data, - Comment: desiredRecord.Comment, - }) + newDNSRecord, err := createCloudflareDNSRecord(ctx, cloudflareAPI, zone.Status.ID, desiredRecord) if err != nil { intconditions.MarkFalse(dnsrecord, err) return ctrl.Result{RequeueAfter: r.RetryInterval}, nil } dnsrecord.Status.RecordID = newDNSRecord.ID } else if !r.compareDNSRecord(desiredRecord, existingRecord) { - proxied := proxiedPtr(proxiedEnabled(desiredRecord.Proxied)) - if _, err := cloudflareAPI.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone.Status.ID), cloudflare.UpdateDNSRecordParams{ - ID: dnsrecord.Status.RecordID, - Name: desiredRecord.Name, - Type: desiredRecord.Type, - Content: desiredRecord.Content, - TTL: desiredRecord.TTL, - Proxied: proxied, - Priority: desiredRecord.Priority, - Data: desiredRecord.Data, - Comment: cloudflare.StringPtr(desiredRecord.Comment), - }); err != nil { + if err := editCloudflareDNSRecord(ctx, cloudflareAPI, zone.Status.ID, dnsrecord.Status.RecordID, desiredRecord); err != nil { intconditions.MarkFalse(dnsrecord, err) return ctrl.Result{RequeueAfter: r.RetryInterval}, nil } @@ -264,11 +246,11 @@ func (r *DNSRecordReconciler) reconcileDNSRecord(ctx context.Context, dnsrecord } // compareDNSRecord compares the DNS record to the DNSRecord object -func (r *DNSRecordReconciler) compareDNSRecord(dnsRecordSpec cloudflareoperatoriov1.DNSRecordSpec, existingRecord cloudflare.DNSRecord) bool { +func (r *DNSRecordReconciler) compareDNSRecord(dnsRecordSpec cloudflareoperatoriov1.DNSRecordSpec, existingRecord dns.RecordResponse) bool { if dnsRecordSpec.Name != existingRecord.Name { return false } - if dnsRecordSpec.Type != existingRecord.Type { + if dnsRecordSpec.Type != string(existingRecord.Type) { return false } if dnsRecordSpec.Type != "SRV" && dnsRecordSpec.Type != "LOC" && dnsRecordSpec.Type != "CAA" { @@ -276,10 +258,10 @@ func (r *DNSRecordReconciler) compareDNSRecord(dnsRecordSpec cloudflareoperatori return false } } - if dnsRecordSpec.TTL != existingRecord.TTL { + if normalizedTTL(dnsRecordSpec.TTL) != float64(existingRecord.TTL) { return false } - if proxiedEnabled(dnsRecordSpec.Proxied) != proxiedEnabled(existingRecord.Proxied) { + if proxiedEnabled(dnsRecordSpec.Proxied) != existingRecord.Proxied { return false } if !comparePriority(dnsRecordSpec.Priority, existingRecord.Priority) { @@ -296,15 +278,12 @@ func (r *DNSRecordReconciler) compareDNSRecord(dnsRecordSpec cloudflareoperatori } // comparePriority compares the priority nil safe -func comparePriority(a, b *uint16) bool { - if a == nil && b == nil { - return true - } - if a == nil || b == nil { - return false +func comparePriority(a *uint16, b float64) bool { + if a == nil { + return b == 0 } - return *a == *b + return float64(*a) == b } // compareData compares the data nil safe @@ -320,7 +299,16 @@ func compareData(a any, b *apiextensionsv1.JSON) bool { return false } - return reflect.DeepEqual(a, bb) + var aa any + aBytes, err := json.Marshal(a) + if err != nil { + return false + } + if err := json.Unmarshal(aBytes, &aa); err != nil { + return false + } + + return reflect.DeepEqual(aa, bb) } func proxiedEnabled(proxied *bool) bool { @@ -334,16 +322,16 @@ func proxiedPtr(proxied bool) *bool { return &proxied } -func findExistingRecordForAdoption(desiredRecord cloudflareoperatoriov1.DNSRecordSpec, existingRecords []cloudflare.DNSRecord) (cloudflare.DNSRecord, error) { +func findExistingRecordForAdoption(desiredRecord cloudflareoperatoriov1.DNSRecordSpec, existingRecords []dns.RecordResponse) (dns.RecordResponse, error) { switch len(existingRecords) { case 0: - return cloudflare.DNSRecord{}, nil + return dns.RecordResponse{}, nil case 1: return existingRecords[0], nil } for _, record := range existingRecords { - if desiredRecord.Name != record.Name || desiredRecord.Type != record.Type { + if desiredRecord.Name != record.Name || desiredRecord.Type != string(record.Type) { continue } if desiredRecord.Type != "SRV" && desiredRecord.Type != "LOC" && desiredRecord.Type != "CAA" && desiredRecord.Content != record.Content { @@ -352,7 +340,7 @@ func findExistingRecordForAdoption(desiredRecord cloudflareoperatoriov1.DNSRecor return record, nil } - return cloudflare.DNSRecord{}, fmt.Errorf("multiple Cloudflare records matched %s %s; set status.recordID manually or remove duplicates", desiredRecord.Type, desiredRecord.Name) + return dns.RecordResponse{}, fmt.Errorf("multiple Cloudflare records matched %s %s; set status.recordID manually or remove duplicates", desiredRecord.Type, desiredRecord.Name) } func resolvedIPAddress(ip *cloudflareoperatoriov1.IP) string { @@ -493,7 +481,7 @@ func (r *DNSRecordReconciler) reconcileDelete(ctx context.Context, zoneID string return err } - if err := cloudflareAPI.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), dnsrecord.Status.RecordID); err != nil && err.Error() != "Record does not exist. (81044)" && dnsrecord.Status.RecordID != "" { + if err := deleteCloudflareDNSRecord(ctx, cloudflareAPI, zoneID, dnsrecord.Status.RecordID); err != nil && !isCloudflareDNSRecordNotFound(err) { return err } metrics.DnsRecordFailureCounter.DeleteLabelValues(dnsrecord.Namespace, dnsrecord.Name, dnsrecord.Spec.Name) diff --git a/internal/controller/dnsrecord_controller_test.go b/internal/controller/dnsrecord_controller_test.go index 1c6d934d..4f938331 100644 --- a/internal/controller/dnsrecord_controller_test.go +++ b/internal/controller/dnsrecord_controller_test.go @@ -21,7 +21,7 @@ import ( "os" "testing" - "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go/v7/dns" "github.com/fluxcd/pkg/runtime/conditions" . "github.com/onsi/gomega" @@ -57,7 +57,7 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { dnsRecord := &cloudflareoperatoriov1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: "dnsrecord", - Namespace: "default", + Namespace: testDefaultNamespace, }, } @@ -66,7 +66,7 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { Name: "ip", }, Status: cloudflareoperatoriov1.IPStatus{ - Address: "2.2.2.2", + Address: testAlternateIPv4Address, }, } secret, account := NewTestAccountObjects() @@ -81,8 +81,8 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { t.Run("reconcile dnsrecord", func(t *testing.T) { g := NewWithT(t) dnsRecord.Spec = cloudflareoperatoriov1.DNSRecordSpec{ - Name: "dnstest.containeroo-test.org", - Content: "1.1.1.1", + Name: testDNSRecordHost, + Content: testIPv4Address, Type: "A", Proxied: new(bool), } @@ -94,21 +94,21 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "DNS record synced"), })) - cloudflareDNSRecord, err := cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), dnsRecord.Status.RecordID) + cloudflareDNSRecord, err := getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, dnsRecord.Status.RecordID) g.Expect(err).ToNot(HaveOccurred()) g.Expect(dnsRecord.Status.RecordID).To(Equal(cloudflareDNSRecord.ID)) _ = r.reconcileDelete(context.TODO(), zone.Status.ID, dnsRecord) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), dnsRecord.Status.RecordID) - g.Expect(err.Error()).To(ContainSubstring("Record does not exist")) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, dnsRecord.Status.RecordID) + g.Expect(err).To(HaveOccurred()) }) t.Run("reconcile dnsrecord with ipref", func(t *testing.T) { g := NewWithT(t) dnsRecord.Status = cloudflareoperatoriov1.DNSRecordStatus{} dnsRecord.Spec = cloudflareoperatoriov1.DNSRecordSpec{ - Name: "dnstest.containeroo-test.org", + Name: testDNSRecordHost, Type: "A", Proxied: new(bool), IPRef: cloudflareoperatoriov1.DNSRecordSpecIPRef{ @@ -123,7 +123,7 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "DNS record synced"), })) - cloudflareDNSRecord, err := cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), dnsRecord.Status.RecordID) + cloudflareDNSRecord, err := getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, dnsRecord.Status.RecordID) g.Expect(err).ToNot(HaveOccurred()) g.Expect(dnsRecord.Status.RecordID).To(Equal(cloudflareDNSRecord.ID)) @@ -131,16 +131,16 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { g.Expect(dnsRecord.Spec.Content).To(BeEmpty()) _ = r.reconcileDelete(context.TODO(), zone.Status.ID, dnsRecord) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), dnsRecord.Status.RecordID) - g.Expect(err.Error()).To(ContainSubstring("Record does not exist")) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, dnsRecord.Status.RecordID) + g.Expect(err).To(HaveOccurred()) }) t.Run("adopt existing dns record", func(t *testing.T) { g := NewWithT(t) - cloudflareDNSRecord, err := cloudflareAPI.CreateDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), cloudflare.CreateDNSRecordParams{ + cloudflareDNSRecord, err := createCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, cloudflareoperatoriov1.DNSRecordSpec{ Name: "adopt.containeroo-test.org", Type: "A", - Content: "1.1.1.1", + Content: testIPv4Address, Proxied: new(bool), }) g.Expect(err).ToNot(HaveOccurred()) @@ -148,9 +148,9 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { dnsRecord.Status = cloudflareoperatoriov1.DNSRecordStatus{} dnsRecord.Spec = cloudflareoperatoriov1.DNSRecordSpec{ Name: cloudflareDNSRecord.Name, - Type: cloudflareDNSRecord.Type, + Type: string(cloudflareDNSRecord.Type), Content: cloudflareDNSRecord.Content, - Proxied: cloudflareDNSRecord.Proxied, + Proxied: proxiedPtr(cloudflareDNSRecord.Proxied), } _, err = r.reconcileDNSRecord(context.TODO(), dnsRecord, zone) @@ -163,17 +163,18 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { g.Expect(dnsRecord.Status.RecordID).To(Equal(cloudflareDNSRecord.ID)) _ = r.reconcileDelete(context.TODO(), zone.Status.ID, dnsRecord) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), dnsRecord.Status.RecordID) - g.Expect(err.Error()).To(ContainSubstring("Record does not exist")) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, dnsRecord.Status.RecordID) + g.Expect(err).To(HaveOccurred()) }) t.Run("compare dns record", func(t *testing.T) { g := NewWithT(t) dnsRecordSpec := cloudflareoperatoriov1.DNSRecordSpec{ - Name: "dnstest.containeroo-test.org", + Name: testDNSRecordHost, Type: "A", - Content: "1.1.1.1", + Content: testIPv4Address, + TTL: 1, Proxied: &[]bool{true}[0], Priority: &[]uint16{10}[0], Data: &v1.JSON{ @@ -182,12 +183,13 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { Comment: "This is a comment", } - cloudflareDNSRecord := cloudflare.DNSRecord{ + cloudflareDNSRecord := dns.RecordResponse{ Name: dnsRecordSpec.Name, - Type: dnsRecordSpec.Type, + Type: dns.RecordResponseType(dnsRecordSpec.Type), Content: dnsRecordSpec.Content, - Proxied: dnsRecordSpec.Proxied, - Priority: dnsRecordSpec.Priority, + TTL: dns.TTL(dnsRecordSpec.TTL), + Proxied: proxiedEnabled(dnsRecordSpec.Proxied), + Priority: float64(*dnsRecordSpec.Priority), Data: map[string]any{"key": "value"}, Comment: dnsRecordSpec.Comment, } @@ -200,14 +202,16 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { g := NewWithT(t) isEqual := r.compareDNSRecord(cloudflareoperatoriov1.DNSRecordSpec{ - Name: "dnstest.containeroo-test.org", + Name: testDNSRecordHost, Type: "A", - Content: "1.1.1.1", - }, cloudflare.DNSRecord{ - Name: "dnstest.containeroo-test.org", + Content: testIPv4Address, + TTL: 1, + }, dns.RecordResponse{ + Name: testDNSRecordHost, Type: "A", - Content: "1.1.1.1", - Proxied: proxiedPtr(true), + Content: testIPv4Address, + TTL: 1, + Proxied: true, }) g.Expect(isEqual).To(BeTrue()) @@ -219,13 +223,13 @@ func TestDNSRecordReconciler_reconcileDNSRecord(t *testing.T) { record, err := findExistingRecordForAdoption(cloudflareoperatoriov1.DNSRecordSpec{ Name: "derived.containeroo-test.org", Type: "A", - Content: "2.2.2.2", - }, []cloudflare.DNSRecord{ + Content: testAlternateIPv4Address, + }, []dns.RecordResponse{ { ID: "derived-record", Name: "derived.containeroo-test.org", Type: "A", - Content: "2.2.2.2", + Content: testAlternateIPv4Address, }, }) g.Expect(err).ToNot(HaveOccurred()) diff --git a/internal/controller/grpcroute_controller_test.go b/internal/controller/grpcroute_controller_test.go index 2fdf3efa..6f6ed7ad 100644 --- a/internal/controller/grpcroute_controller_test.go +++ b/internal/controller/grpcroute_controller_test.go @@ -34,9 +34,9 @@ func TestGRPCRouteReconciler_reconcileGRPCRoute(t *testing.T) { grpcRoute := &gatewayv1.GRPCRoute{ ObjectMeta: metav1.ObjectMeta{ Name: "grpcroute", - Namespace: "default", + Namespace: testDefaultNamespace, Annotations: map[string]string{ - "cloudflare-operator.io/content": "1.1.1.1", + testContentAnnotation: testIPv4Address, }, }, Spec: gatewayv1.GRPCRouteSpec{ @@ -73,43 +73,43 @@ func TestGRPCRouteReconciler_reconcileGRPCRoute(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "grpc-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "grpc-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("grpc.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("1.1.1.1"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testIPv4Address))) }) t.Run("change dnsrecord spec when annotations change", func(t *testing.T) { g := NewWithT(t) grpcRoute.Annotations = map[string]string{ - "cloudflare-operator.io/content": "2.2.2.2", + testContentAnnotation: testAlternateIPv4Address, } _, err := r.reconcileGRPCRoute(context.TODO(), grpcRoute) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "grpc-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "grpc-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("grpc.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("reconcile grpcroute wildcard", func(t *testing.T) { g := NewWithT(t) - grpcRoute.Spec.Hostnames = []gatewayv1.Hostname{"*.containeroo-test.org"} + grpcRoute.Spec.Hostnames = []gatewayv1.Hostname{testWildcardHost} _, err := r.reconcileGRPCRoute(context.TODO(), grpcRoute) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("*.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal(testWildcardHost))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("remove dnsrecord when annotations are absent", func(t *testing.T) { @@ -120,7 +120,7 @@ func TestGRPCRouteReconciler_reconcileGRPCRoute(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) } diff --git a/internal/controller/httproute_controller_test.go b/internal/controller/httproute_controller_test.go index 8271f872..62c19048 100644 --- a/internal/controller/httproute_controller_test.go +++ b/internal/controller/httproute_controller_test.go @@ -35,9 +35,9 @@ func TestHTTPRouteReconciler_reconcileHTTPRoute(t *testing.T) { httpRoute := &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ Name: "httproute", - Namespace: "default", + Namespace: testDefaultNamespace, Annotations: map[string]string{ - "cloudflare-operator.io/content": "1.1.1.1", + testContentAnnotation: testIPv4Address, }, }, Spec: gatewayv1.HTTPRouteSpec{ @@ -74,43 +74,43 @@ func TestHTTPRouteReconciler_reconcileHTTPRoute(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "app-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "app-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("app.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("1.1.1.1"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testIPv4Address))) }) t.Run("change dnsrecord spec when annotations change", func(t *testing.T) { g := NewWithT(t) httpRoute.Annotations = map[string]string{ - "cloudflare-operator.io/content": "2.2.2.2", + testContentAnnotation: testAlternateIPv4Address, } _, err := r.reconcileHTTPRoute(context.TODO(), httpRoute) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "app-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "app-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("app.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("reconcile httproute wildcard", func(t *testing.T) { g := NewWithT(t) - httpRoute.Spec.Hostnames = []gatewayv1.Hostname{"*.containeroo-test.org"} + httpRoute.Spec.Hostnames = []gatewayv1.Hostname{testWildcardHost} _, err := r.reconcileHTTPRoute(context.TODO(), httpRoute) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("*.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal(testWildcardHost))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("remove dnsrecord when annotations are absent", func(t *testing.T) { @@ -121,7 +121,7 @@ func TestHTTPRouteReconciler_reconcileHTTPRoute(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) } diff --git a/internal/controller/ingress_controller_test.go b/internal/controller/ingress_controller_test.go index 34ae1bcf..d927b5f2 100644 --- a/internal/controller/ingress_controller_test.go +++ b/internal/controller/ingress_controller_test.go @@ -35,9 +35,9 @@ func TestIngressReconciler_reconcileIngress(t *testing.T) { ingress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "ingress", - Namespace: "default", + Namespace: testDefaultNamespace, Annotations: map[string]string{ - "cloudflare-operator.io/content": "1.1.1.1", + testContentAnnotation: testIPv4Address, }, }, } @@ -71,46 +71,46 @@ func TestIngressReconciler_reconcileIngress(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "ingtest-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "ingtest-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("ingtest.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("1.1.1.1"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testIPv4Address))) }) t.Run("change dnsrecord spec when annotations change", func(t *testing.T) { g := NewWithT(t) ingress.Annotations = map[string]string{ - "cloudflare-operator.io/content": "2.2.2.2", + testContentAnnotation: testAlternateIPv4Address, } _, err := r.reconcileIngress(context.TODO(), ingress) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "ingtest-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "ingtest-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("ingtest.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("reconcile ingress wildcard", func(t *testing.T) { g := NewWithT(t) ingress.Spec = networkingv1.IngressSpec{ Rules: []networkingv1.IngressRule{{ - Host: "*.containeroo-test.org", + Host: testWildcardHost, }}, } _, err := r.reconcileIngress(context.TODO(), ingress) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("*.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal(testWildcardHost))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("remove dnsrecord when annotations are absent", func(t *testing.T) { @@ -121,15 +121,15 @@ func TestIngressReconciler_reconcileIngress(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) t.Run("ingress annotation parsing", func(t *testing.T) { g := NewWithT(t) ingress.Annotations = map[string]string{ - "cloudflare-operator.io/account-ref": "account", - "cloudflare-operator.io/content": "1.1.1.1", + "cloudflare-operator.io/account-ref": testAccountName, + testContentAnnotation: testIPv4Address, "cloudflare-operator.io/ip-ref": "ip", "cloudflare-operator.io/proxied": "true", "cloudflare-operator.io/ttl": "120", // Expecting to return 1 because proxied is true @@ -139,8 +139,8 @@ func TestIngressReconciler_reconcileIngress(t *testing.T) { parsedSpec := parseDNSAnnotations(ingress.Annotations, 30*time.Second) - g.Expect(parsedSpec.AccountRef).To(HaveField("Name", Equal("account"))) - g.Expect(parsedSpec).To(HaveField("Content", Equal("1.1.1.1"))) + g.Expect(parsedSpec.AccountRef).To(HaveField("Name", Equal(testAccountName))) + g.Expect(parsedSpec).To(HaveField("Content", Equal(testIPv4Address))) g.Expect(parsedSpec.IPRef).To(HaveField("Name", Equal("ip"))) g.Expect(parsedSpec).To(HaveField("Proxied", Equal(&[]bool{true}[0]))) g.Expect(parsedSpec).To(HaveField("TTL", Equal(1))) diff --git a/internal/controller/ip_controller_test.go b/internal/controller/ip_controller_test.go index 11bd2029..2c1a6d12 100644 --- a/internal/controller/ip_controller_test.go +++ b/internal/controller/ip_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "context" + "fmt" "net/http" "testing" @@ -40,18 +41,18 @@ var ( func StartIPSource() { http.HandleFunc("/plain", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("1.1.1.1")) + _, _ = w.Write([]byte(testIPv4Address)) }) http.HandleFunc("/invalid", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("invalid")) }) http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(`{"ip":"1.1.1.1"}`)) + _, _ = fmt.Fprintf(w, `{"ip":"%s"}`, testIPv4Address) }) http.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) { requestHeader = r.Header.Get("X-Test") requestAuthHeader = r.Header.Get("X-Auth-Test") - _, _ = w.Write([]byte("1.1.1.1")) + _, _ = w.Write([]byte(testIPv4Address)) }) _ = http.ListenAndServe(":8080", nil) @@ -62,8 +63,8 @@ func TestIPReconciler_reconcileIP(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", + Name: testSecretName, + Namespace: testDefaultNamespace, }, Data: map[string][]byte{ "X-Auth-Test": []byte("auth-test"), @@ -99,7 +100,7 @@ func TestIPReconciler_reconcileIP(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "IP is ready"), })) - g.Expect(ip.Status.Address).To(Equal("1.1.1.1")) + g.Expect(ip.Status.Address).To(Equal(testIPv4Address)) g.Expect(ip.Spec.Address).To(BeEmpty()) g.Expect(ip.Spec.Interval).To(BeNil()) }) @@ -140,7 +141,7 @@ func TestIPReconciler_reconcileIP(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "IP is ready"), })) - g.Expect(ip.Status.Address).To(Equal("1.1.1.1")) + g.Expect(ip.Status.Address).To(Equal(testIPv4Address)) g.Expect(ip.Spec.Address).To(BeEmpty()) }) @@ -156,7 +157,7 @@ func TestIPReconciler_reconcileIP(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "IP is ready"), })) - g.Expect(ip.Status.Address).To(Equal("1.1.1.1")) + g.Expect(ip.Status.Address).To(Equal(testIPv4Address)) g.Expect(ip.Spec.Address).To(BeEmpty()) }) @@ -174,7 +175,7 @@ func TestIPReconciler_reconcileIP(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "IP is ready"), })) - g.Expect(ip.Status.Address).To(Equal("1.1.1.1")) + g.Expect(ip.Status.Address).To(Equal(testIPv4Address)) g.Expect(ip.Spec.Address).To(BeEmpty()) g.Expect(requestHeader).To(Equal("test")) }) @@ -183,8 +184,8 @@ func TestIPReconciler_reconcileIP(t *testing.T) { ip.Spec.IPSources = []cloudflareoperatoriov1.IPSpecIPSources{{ URL: "http://localhost:8080/header", RequestHeadersSecretRef: corev1.SecretReference{ - Name: "secret", - Namespace: "default", + Name: testSecretName, + Namespace: testDefaultNamespace, }, }} @@ -194,22 +195,22 @@ func TestIPReconciler_reconcileIP(t *testing.T) { *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionReasonReady, "IP is ready"), })) - g.Expect(ip.Status.Address).To(Equal("1.1.1.1")) + g.Expect(ip.Status.Address).To(Equal(testIPv4Address)) g.Expect(ip.Spec.Address).To(BeEmpty()) g.Expect(requestAuthHeader).To(Equal("auth-test")) }) t.Run("reconcile static ip", func(t *testing.T) { ip.Spec.Type = "static" - ip.Spec.Address = "1.1.1.1" + ip.Spec.Address = testIPv4Address _ = r.reconcileIP(context.TODO(), ip) g.Expect(ip.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ *conditions.TrueCondition(cloudflareoperatoriov1.ConditionTypeReady, cloudflareoperatoriov1.ConditionTypeReady, "IP is ready"), })) - g.Expect(ip.Status.Address).To(Equal("1.1.1.1")) - g.Expect(ip.Spec.Address).To(Equal("1.1.1.1")) + g.Expect(ip.Status.Address).To(Equal(testIPv4Address)) + g.Expect(ip.Spec.Address).To(Equal(testIPv4Address)) }) t.Run("reconcile static ip error no address", func(t *testing.T) { diff --git a/internal/controller/tlsroute_controller_test.go b/internal/controller/tlsroute_controller_test.go index 4f3906d1..0b2788b9 100644 --- a/internal/controller/tlsroute_controller_test.go +++ b/internal/controller/tlsroute_controller_test.go @@ -34,9 +34,9 @@ func TestTLSRouteReconciler_reconcileTLSRoute(t *testing.T) { tlsRoute := &gatewayv1.TLSRoute{ ObjectMeta: metav1.ObjectMeta{ Name: "tlsroute", - Namespace: "default", + Namespace: testDefaultNamespace, Annotations: map[string]string{ - "cloudflare-operator.io/content": "1.1.1.1", + testContentAnnotation: testIPv4Address, }, }, Spec: gatewayv1.TLSRouteSpec{ @@ -73,43 +73,43 @@ func TestTLSRouteReconciler_reconcileTLSRoute(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "tls-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "tls-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("tls.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("1.1.1.1"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testIPv4Address))) }) t.Run("change dnsrecord spec when annotations change", func(t *testing.T) { g := NewWithT(t) tlsRoute.Annotations = map[string]string{ - "cloudflare-operator.io/content": "2.2.2.2", + testContentAnnotation: testAlternateIPv4Address, } _, err := r.reconcileTLSRoute(context.TODO(), tlsRoute) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "tls-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: "tls-containeroo-test-org"}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("tls.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("reconcile tlsroute wildcard", func(t *testing.T) { g := NewWithT(t) - tlsRoute.Spec.Hostnames = []gatewayv1.Hostname{"*.containeroo-test.org"} + tlsRoute.Spec.Hostnames = []gatewayv1.Hostname{testWildcardHost} _, err := r.reconcileTLSRoute(context.TODO(), tlsRoute) g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal("*.containeroo-test.org"))) - g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal("2.2.2.2"))) + g.Expect(dnsRecord.Spec).To(HaveField("Name", Equal(testWildcardHost))) + g.Expect(dnsRecord.Spec).To(HaveField("Content", Equal(testAlternateIPv4Address))) }) t.Run("remove dnsrecord when annotations are absent", func(t *testing.T) { @@ -120,7 +120,7 @@ func TestTLSRouteReconciler_reconcileTLSRoute(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) dnsRecord := &cloudflareoperatoriov1.DNSRecord{} - err = r.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "wildcard-containeroo-test-org"}, dnsRecord) + err = r.Get(context.TODO(), client.ObjectKey{Namespace: testDefaultNamespace, Name: testWildcardDNSRecordName}, dnsRecord) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) } diff --git a/internal/controller/zone_controller.go b/internal/controller/zone_controller.go index 0bdd7ebc..c7dc813f 100644 --- a/internal/controller/zone_controller.go +++ b/internal/controller/zone_controller.go @@ -24,7 +24,8 @@ import ( "strings" "time" - "github.com/cloudflare/cloudflare-go" + 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" apierrutil "k8s.io/apimachinery/pkg/util/errors" @@ -143,7 +144,7 @@ func (r *ZoneReconciler) reconcileZone(ctx context.Context, zone *cloudflareoper return ctrl.Result{RequeueAfter: r.RetryInterval}, nil } - zoneID, err := cloudflareAPI.ZoneIDByName(zone.Spec.Name) + zoneID, err := cloudflareZoneIDByName(ctx, cloudflareAPI, zone.Spec.Name) if err != nil { intconditions.MarkFalse(zone, err) return ctrl.Result{RequeueAfter: r.RetryInterval}, errWaitForZone @@ -164,7 +165,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.API, zone *cloudflareoperatoriov1.Zone) error { +func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudflare.Client, zone *cloudflareoperatoriov1.Zone) error { log := ctrl.LoggerFrom(ctx) zones := &cloudflareoperatoriov1.ZoneList{} @@ -179,7 +180,7 @@ func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudfl return client.IgnoreNotFound(err) } - cloudflareDNSRecords, _, err := cloudflareAPI.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zone.Status.ID), cloudflare.ListDNSRecordsParams{}) + cloudflareDNSRecords, err := listCloudflareDNSRecords(ctx, cloudflareAPI, zone.Status.ID, dns.RecordListParams{}) if err != nil { intconditions.MarkFalse(zone, err) return err @@ -188,7 +189,8 @@ func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudfl dnsRecordMap, dnsRecordSpecMap := managedDNSRecordKeysForZone(zone, dnsRecords.Items, zones.Items) for _, cloudflareDNSRecord := range cloudflareDNSRecords { - if patterns, found := zone.Spec.IgnoredRecords[cloudflareDNSRecord.Type]; found && + recordType := string(cloudflareDNSRecord.Type) + if patterns, found := zone.Spec.IgnoredRecords[recordType]; found && matchesIgnored(cloudflareDNSRecord.Name, patterns, ctx) { continue } @@ -196,11 +198,11 @@ func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudfl if _, found := dnsRecordMap[cloudflareDNSRecord.ID]; found { continue } - if _, found := dnsRecordSpecMap[dnsRecordKey(cloudflareDNSRecord.Type, cloudflareDNSRecord.Name)]; found { + if _, found := dnsRecordSpecMap[dnsRecordKey(recordType, cloudflareDNSRecord.Name)]; found { continue } - if err := cloudflareAPI.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zone.Status.ID), cloudflareDNSRecord.ID); err != nil && err.Error() != "Record does not exist. (81044)" { + if err := deleteCloudflareDNSRecord(ctx, cloudflareAPI, zone.Status.ID, cloudflareDNSRecord.ID); err != nil && !isCloudflareDNSRecordNotFound(err) { return err } log.Info("Deleted DNS record on Cloudflare", "name", cloudflareDNSRecord.Name) diff --git a/internal/controller/zone_controller_test.go b/internal/controller/zone_controller_test.go index 2f043daf..846dd6ad 100644 --- a/internal/controller/zone_controller_test.go +++ b/internal/controller/zone_controller_test.go @@ -21,7 +21,7 @@ import ( "os" "testing" - "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go/v7/dns" "github.com/fluxcd/pkg/runtime/conditions" . "github.com/onsi/gomega" @@ -53,16 +53,17 @@ func TestZoneReconciler_reconcileZone(t *testing.T) { } zoneID := os.Getenv("CF_ZONE_ID") - var testRecord cloudflare.DNSRecord + var testRecord dns.RecordResponse t.Run("create dns record for testing", func(t *testing.T) { g := NewWithT(t) var err error - testRecord, err = cloudflareAPI.CreateDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), cloudflare.CreateDNSRecordParams{ + testRecord, err = createCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, cloudflareoperatoriov1.DNSRecordSpec{ Name: "test.containeroo-test.org", - Content: "1.1.1.1", + Content: testIPv4Address, Type: "A", + Proxied: new(bool), }) g.Expect(err).ToNot(HaveOccurred()) }) @@ -80,7 +81,7 @@ func TestZoneReconciler_reconcileZone(t *testing.T) { })) g.Expect(zone.Status.ID).To(Equal(zoneID)) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), testRecord.ID) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, testRecord.ID) g.Expect(err).ToNot(HaveOccurred()) }) @@ -89,46 +90,49 @@ func TestZoneReconciler_reconcileZone(t *testing.T) { zone.Spec.Prune = true zone.Spec.IgnoredRecords = map[string][]string{ - "TXT": {"_acme-challenge", "cf2024-1._domainkey"}, - "A": {"^mytest.*$"}, + testRecordTypeTXT: {"_acme-challenge", "cf2024-1._domainkey"}, + "A": {"^mytest.*$"}, } - acmeRecord, err := cloudflareAPI.CreateDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), cloudflare.CreateDNSRecordParams{ + acmeRecord, err := createCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, cloudflareoperatoriov1.DNSRecordSpec{ Name: "_acme-challenge.abc.containeroo-test.org", - Type: "TXT", + Type: testRecordTypeTXT, Content: "test", + Proxied: new(bool), }) g.Expect(err).ToNot(HaveOccurred()) - dkimRecord, err := cloudflareAPI.CreateDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), cloudflare.CreateDNSRecordParams{ + dkimRecord, err := createCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, cloudflareoperatoriov1.DNSRecordSpec{ Name: "cf2024-1._domainkey.containeroo-test.org", - Type: "TXT", + Type: testRecordTypeTXT, Content: "test", + Proxied: new(bool), }) g.Expect(err).ToNot(HaveOccurred()) - aRecord, err := cloudflareAPI.CreateDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), cloudflare.CreateDNSRecordParams{ + aRecord, err := createCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, cloudflareoperatoriov1.DNSRecordSpec{ Name: "mytestabc.containeroo-test.org", Type: "A", - Content: "1.1.1.1", + Content: testIPv4Address, + Proxied: new(bool), }) g.Expect(err).ToNot(HaveOccurred()) _, _ = r.reconcileZone(context.TODO(), zone) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), testRecord.ID) - g.Expect(err.Error()).To(ContainSubstring("Record does not exist")) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, testRecord.ID) + g.Expect(err).To(HaveOccurred()) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), acmeRecord.ID) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, acmeRecord.ID) g.Expect(err).ToNot(HaveOccurred()) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), dkimRecord.ID) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, dkimRecord.ID) g.Expect(err).ToNot(HaveOccurred()) - _, err = cloudflareAPI.GetDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zone.Status.ID), aRecord.ID) + _, err = getCloudflareDNSRecord(context.TODO(), cloudflareAPI, zone.Status.ID, aRecord.ID) g.Expect(err).ToNot(HaveOccurred()) - err = cloudflareAPI.DeleteDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), acmeRecord.ID) + err = deleteCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, acmeRecord.ID) g.Expect(err).ToNot(HaveOccurred()) - err = cloudflareAPI.DeleteDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), dkimRecord.ID) + err = deleteCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, dkimRecord.ID) g.Expect(err).ToNot(HaveOccurred()) - err = cloudflareAPI.DeleteDNSRecord(context.TODO(), cloudflare.ZoneIdentifier(zoneID), aRecord.ID) + err = deleteCloudflareDNSRecord(context.TODO(), cloudflareAPI, zoneID, aRecord.ID) g.Expect(err).ToNot(HaveOccurred()) }) diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index e3e5ea06..1a1be33b 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -21,34 +21,36 @@ import ( k8smetrics "sigs.k8s.io/controller-runtime/pkg/metrics" ) +const labelName = "name" + var ( AccountFailureCounter = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "cloudflare_operator_account_status", Help: "Cloudflare account status", }, - []string{"name"}, + []string{labelName}, ) DnsRecordFailureCounter = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "cloudflare_operator_dns_record_status", Help: "Cloudflare DNS records status", }, - []string{"namespace", "name", "record_name"}, + []string{"namespace", labelName, "record_name"}, ) IpFailureCounter = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "cloudflare_operator_ip_status", Help: "IPs status", }, - []string{"name", "ip_type"}, + []string{labelName, "ip_type"}, ) ZoneFailureCounter = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "cloudflare_operator_zone_status", Help: "Cloudflare zones status", }, - []string{"name", "zone_name"}, + []string{labelName, "zone_name"}, ) ) diff --git a/internal/predicates/predicates_test.go b/internal/predicates/predicates_test.go index 855ea3a6..e86a73a8 100644 --- a/internal/predicates/predicates_test.go +++ b/internal/predicates/predicates_test.go @@ -29,6 +29,17 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) +const ( + testAnnotationValue = "test" + testContentAnnotation = "cloudflare-operator.io/content" + testGRPCRouteName = "grpcroute" + testHost = "test.containeroo-test.org" + testHTTPRouteName = "httproute" + testIngressName = "ingress" + testNewHost = "test-new.containeroo-test.org" + testTLSRouteName = "tlsroute" +) + func TestPredicate(t *testing.T) { predicate := DNSFromIngressPredicate{} @@ -37,7 +48,7 @@ func TestPredicate(t *testing.T) { ingress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress", + Name: testIngressName, }, } @@ -51,9 +62,9 @@ func TestPredicate(t *testing.T) { ingress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress", + Name: testIngressName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, } @@ -69,28 +80,28 @@ func TestPredicate(t *testing.T) { oldIngress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress", + Name: testIngressName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: networkingv1.IngressSpec{ Rules: []networkingv1.IngressRule{{ - Host: "test.containeroo-test.org", + Host: testHost, }}, }, } newIngress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress", + Name: testIngressName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "new-test", + testContentAnnotation: "new-test", }, }, Spec: networkingv1.IngressSpec{ Rules: []networkingv1.IngressRule{{ - Host: "test.containeroo-test.org", + Host: testHost, }}, }, } @@ -106,28 +117,28 @@ func TestPredicate(t *testing.T) { oldIngress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress", + Name: testIngressName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: networkingv1.IngressSpec{ Rules: []networkingv1.IngressRule{{ - Host: "test.containeroo-test.org", + Host: testHost, }}, }, } newIngress := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress", + Name: testIngressName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: networkingv1.IngressSpec{ Rules: []networkingv1.IngressRule{{ - Host: "test-new.containeroo-test.org", + Host: testNewHost, }}, }, } @@ -143,9 +154,9 @@ func TestPredicate(t *testing.T) { httpRoute := &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "httproute", + Name: testHTTPRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, } @@ -161,25 +172,25 @@ func TestPredicate(t *testing.T) { oldHTTPRoute := &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "httproute", + Name: testHTTPRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: gatewayv1.HTTPRouteSpec{ - Hostnames: []gatewayv1.Hostname{"test.containeroo-test.org"}, + Hostnames: []gatewayv1.Hostname{testHost}, }, } newHTTPRoute := &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "httproute", + Name: testHTTPRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: gatewayv1.HTTPRouteSpec{ - Hostnames: []gatewayv1.Hostname{"test-new.containeroo-test.org"}, + Hostnames: []gatewayv1.Hostname{testNewHost}, }, } @@ -194,9 +205,9 @@ func TestPredicate(t *testing.T) { tlsRoute := &gatewayv1.TLSRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "tlsroute", + Name: testTLSRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, } @@ -212,25 +223,25 @@ func TestPredicate(t *testing.T) { oldTLSRoute := &gatewayv1.TLSRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "tlsroute", + Name: testTLSRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: gatewayv1.TLSRouteSpec{ - Hostnames: []gatewayv1.Hostname{"test.containeroo-test.org"}, + Hostnames: []gatewayv1.Hostname{testHost}, }, } newTLSRoute := &gatewayv1.TLSRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "tlsroute", + Name: testTLSRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: gatewayv1.TLSRouteSpec{ - Hostnames: []gatewayv1.Hostname{"test-new.containeroo-test.org"}, + Hostnames: []gatewayv1.Hostname{testNewHost}, }, } @@ -245,9 +256,9 @@ func TestPredicate(t *testing.T) { grpcRoute := &gatewayv1.GRPCRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "grpcroute", + Name: testGRPCRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, } @@ -263,25 +274,25 @@ func TestPredicate(t *testing.T) { oldGRPCRoute := &gatewayv1.GRPCRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "grpcroute", + Name: testGRPCRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: gatewayv1.GRPCRouteSpec{ - Hostnames: []gatewayv1.Hostname{"test.containeroo-test.org"}, + Hostnames: []gatewayv1.Hostname{testHost}, }, } newGRPCRoute := &gatewayv1.GRPCRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "grpcroute", + Name: testGRPCRouteName, Annotations: map[string]string{ - "cloudflare-operator.io/content": "test", + testContentAnnotation: testAnnotationValue, }, }, Spec: gatewayv1.GRPCRouteSpec{ - Hostnames: []gatewayv1.Hostname{"test-new.containeroo-test.org"}, + Hostnames: []gatewayv1.Hostname{testNewHost}, }, } diff --git a/test/utils/utils.go b/test/utils/utils.go index e02e44ae..cb95cca7 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -24,7 +24,9 @@ import ( "os/exec" "strings" - "github.com/cloudflare/cloudflare-go" + cloudflare "github.com/cloudflare/cloudflare-go/v7" + "github.com/cloudflare/cloudflare-go/v7/dns" + "github.com/cloudflare/cloudflare-go/v7/option" . "github.com/onsi/ginkgo/v2" //nolint:revive,staticcheck ) @@ -183,20 +185,19 @@ func VerifyDNSRecordContent(objName, expectedContent string) error { return fmt.Errorf("dnsrecord has unexpected content: %s", ip) } - api, err := cloudflare.NewWithAPIToken(os.Getenv("CF_API_TOKEN")) - if err != nil { - return fmt.Errorf("failed to create Cloudflare API client: %w", err) - } + api := cloudflare.NewClient(option.WithAPIToken(os.Getenv("CF_API_TOKEN"))) zoneID, err := zoneIDForDNSRecordName(strings.TrimSpace(string(recordName))) if err != nil { return err } - record, err := api.GetDNSRecord( + record, err := api.DNS.Records.Get( context.Background(), - cloudflare.ZoneIdentifier(zoneID), strings.TrimSpace(string(recordID)), + dns.RecordGetParams{ + ZoneID: cloudflare.String(zoneID), + }, ) if err != nil { return fmt.Errorf("failed to get Cloudflare DNS record %s: %w", string(recordID), err)