diff --git a/.github/workflows/devstack.yml b/.github/workflows/devstack.yml index c80f28e..fa63041 100644 --- a/.github/workflows/devstack.yml +++ b/.github/workflows/devstack.yml @@ -70,6 +70,16 @@ jobs: run: ./stack.sh working-directory: ./devstack + - name: Fix Designate root_helper + # root_helper defaults to "sudo designate-rootwrap ..." in designate/conf/base.py. + # designate-rootwrap lives in the devstack venv (/opt/stack/data/venv/bin) which + # is not in sudo's secure_path -> every rndc addzone fails -> zones PENDING->ERROR. + # Insert the absolute path immediately after [DEFAULT] so oslo.config picks it up. + run: | + sudo sed -i '/^\[DEFAULT\]/a root_helper = sudo /opt/stack/data/venv/bin/designate-rootwrap /etc/designate/rootwrap.conf' \ + /etc/designate/designate.conf + shell: bash + - name: Restart Designate run: sudo systemctl restart "devstack@designate-*.service" @@ -79,7 +89,7 @@ jobs: - name: Wait for zone creation run: | - while [ "$(openstack zone list -f csv | grep PENDING)" != "" ]; do date; openstack zone list -f value; sleep 1; done + while [ "$(openstack zone list -f value --status PENDING)" != "" ]; do date; openstack zone list -f value; sleep 1; done - name: Start external-dns-openstack-webhook in background run: | @@ -94,7 +104,7 @@ jobs: - name: Wait for PENDING run: | - while [ "$(openstack zone list -f csv | grep PENDING)" != "" ]; do date; openstack zone list -f value; sleep 1; done + while [ "$(openstack zone list -f value --status PENDING)" != "" ]; do date; openstack zone list -f value; sleep 1; done - name: Show created entries run: | @@ -107,3 +117,31 @@ jobs: run: | if [ $(openstack recordset list all -f value | grep -c " TXT ") -ne 10 ]; then exit 1; fi if [ $(openstack recordset list all -f value | grep -c " A ") -ne 10 ]; then exit 2; fi + + - name: Create zone all-projects.test in alt_demo project + run: | + openstack --os-cloud devstack-alt zone create --email admin@example.com all-projects.test. + + - name: Wait for all-projects.test zone creation + run: | + while [ "$(openstack --os-cloud devstack-admin zone list -f value --status PENDING --all-projects)" != "" ]; do date; openstack --os-cloud devstack-admin zone list -f value --all-projects; sleep 1; done + + - name: Kill external-dns-openstack-webhook + run: pkill -f external-dns-openstack-webhook || true + + - name: Start external-dns-openstack-webhook as admin with --all-projects + run: | + OS_CLOUD=devstack-admin ./build/bin/external-dns-openstack-webhook --all-projects --domain-filter all-projects.test >/tmp/external-dns-openstack-webhook-all-projects.log 2>&1 & + for i in $(seq 30); do curl -sf http://127.0.0.1:8888 > /dev/null 2>&1 && break; sleep 1; done + curl -sf http://127.0.0.1:8888 > /dev/null || { cat /tmp/external-dns-openstack-webhook-all-projects.log; exit 1; } + + - name: Run external-dns against all-projects.test as admin user + run: ./build/external-dns --txt-owner-id all-projects-cluster-id --provider webhook --source fake --fqdn-template=all-projects.test --log-level=debug --once 2>&1 + working-directory: ./external-dns + + - name: Show /tmp/external-dns-openstack-webhook-all-projects.log + run: cat /tmp/external-dns-openstack-webhook-all-projects.log + + - name: Check all-projects created entries in all-projects.test + run: | + if [ $(openstack --os-cloud devstack-admin recordset list all-projects.test. --all-projects -f value | grep -c "all-projects-cluster-id") -ne 10 ]; then exit 1; fi \ No newline at end of file diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 04b701b..fd84741 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -16,13 +16,19 @@ import ( ) const ( - webhookServerAddr = "127.0.0.1:8888" - statusServerAddr = "0.0.0.0:8080" + defaultWebhookServerAddr = "127.0.0.1:8888" + defaultStatusServerAddr = "0.0.0.0:8080" ) func main() { var domainFilters []string + var allProjects bool + var webhookServerAddr string + var statusServerAddr string pflag.StringArrayVar(&domainFilters, "domain-filter", []string{}, "List of domains to work on (can be specified multiple times)") + pflag.BoolVar(&allProjects, "all-projects", false, "Manage all zones and recordsets regardless of the project the service user resides in (requires an admin or appropriately scoped token)") + pflag.StringVar(&webhookServerAddr, "webhook-server-address", defaultWebhookServerAddr, "Address to bind the webhook server to") + pflag.StringVar(&statusServerAddr, "status-server-address", defaultStatusServerAddr, "Address to bind the status and metrics server to") pflag.Parse() log.SetLevel(log.DebugLevel) @@ -61,7 +67,7 @@ func main() { }() epf := endpoint.NewDomainFilter(domainFilters) - dp, err := provider.NewDesignateProvider(*epf, false) + dp, err := provider.NewDesignateProvider(*epf, allProjects, false) if err != nil { log.Fatalf("NewDesignateProvider: %v", err) metrics.OpenstackConnectionMetric.Set(0) diff --git a/internal/designate/client/client.go b/internal/designate/client/client.go index 092d54a..ab09a2f 100644 --- a/internal/designate/client/client.go +++ b/internal/designate/client/client.go @@ -57,15 +57,16 @@ type DesignateClientInterface interface { // implementation of the DesignateClientInterface type designateClient struct { serviceClient *gophercloud.ServiceClient + allProjects bool } // factory function for the DesignateClientInterface -func NewDesignateClient() (DesignateClientInterface, error) { +func NewDesignateClient(allProjects bool) (DesignateClientInterface, error) { serviceClient, err := createDesignateServiceClient() if err != nil { return nil, err } - return &designateClient{serviceClient}, nil + return &designateClient{serviceClient: serviceClient, allProjects: allProjects}, nil } // authenticate in OpenStack and obtain Designate service endpoint @@ -136,10 +137,10 @@ func (c designateClient) ForEachZone(ctx context.Context, filters []string, hand var err error if len(filters) == 0 { - err = doList(zones.ListOpts{}) + err = doList(zones.ListOpts{AllProjects: c.allProjects}) } else { for _, f := range filters { - if err = doList(zones.ListOpts{Name: f + "."}); err != nil { + if err = doList(zones.ListOpts{Name: f + ".", AllProjects: c.allProjects}); err != nil { break } } @@ -162,7 +163,7 @@ func (c designateClient) ForEachZone(ctx context.Context, filters []string, hand func (c designateClient) ForEachRecordSet(ctx context.Context, zoneID string, handler func(recordSet *recordsets.RecordSet) error) error { startTime := time.Now() - pager := recordsets.ListByZone(c.serviceClient, zoneID, recordsets.ListOpts{}) + pager := recordsets.ListByZone(c.serviceClient, zoneID, recordsets.ListOpts{AllProjects: c.allProjects}) var pageCount int var recordCount int @@ -209,6 +210,7 @@ func (c designateClient) CreateRecordSet(ctx context.Context, zoneID string, opt log.Debugf("→ Creating recordset: %s (%s) with %d targets", opts.Name, opts.Type, len(opts.Records)) + opts.AllProjects = c.allProjects r, err := recordsets.Create(ctx, c.serviceClient, zoneID, opts).Extract() duration := time.Since(startTime) @@ -235,6 +237,7 @@ func (c designateClient) UpdateRecordSet(ctx context.Context, zoneID, recordSetI } log.Debugf("→ Updating recordset: %s with %d targets", recordSetID, recordCount) + opts.AllProjects = c.allProjects _, err := recordsets.Update(ctx, c.serviceClient, zoneID, recordSetID, opts).Extract() duration := time.Since(startTime) @@ -257,7 +260,7 @@ func (c designateClient) DeleteRecordSet(ctx context.Context, zoneID, recordSetI log.Debugf("→ Deleting recordset: %s", recordSetID) - err := recordsets.Delete(ctx, c.serviceClient, zoneID, recordSetID).ExtractErr() + err := recordsets.DeleteWithOpts(ctx, c.serviceClient, zoneID, recordSetID, recordsets.DeleteOpts{AllProjects: c.allProjects}).ExtractErr() duration := time.Since(startTime) metrics.ApiCallLatency.WithLabelValues("DeleteRecordSet").Observe(duration.Seconds()) diff --git a/internal/designate/provider/provider.go b/internal/designate/provider/provider.go index bb7a488..f8fbd85 100644 --- a/internal/designate/provider/provider.go +++ b/internal/designate/provider/provider.go @@ -56,8 +56,8 @@ type designateProvider struct { } // NewDesignateProvider is a factory function for OpenStack designate providers -func NewDesignateProvider(domainFilter endpoint.DomainFilter, dryRun bool) (provider.Provider, error) { - client, err := client.NewDesignateClient() +func NewDesignateProvider(domainFilter endpoint.DomainFilter, allProjects, dryRun bool) (provider.Provider, error) { + client, err := client.NewDesignateClient(allProjects) if err != nil { return nil, err } diff --git a/internal/designate/provider/provider_test.go b/internal/designate/provider/provider_test.go index d649c2c..811aa54 100644 --- a/internal/designate/provider/provider_test.go +++ b/internal/designate/provider/provider_test.go @@ -262,7 +262,7 @@ clouds: os.Setenv("OS_CLOUD", "unittest") os.Setenv("OS_CACERT", tmpfile.Name()) - if _, err := NewDesignateProvider(endpoint.DomainFilter{}, true); err != nil { + if _, err := NewDesignateProvider(endpoint.DomainFilter{}, false, true); err != nil { t.Fatalf("Failed to initialize Designate provider: %s", err) } }