Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c31734c
feat: add --all-projects flag to support cross-project zone management
abhijith-darshan May 27, 2026
a894185
feat: pass allProjects flag through to Designate client
abhijith-darshan May 27, 2026
c989be5
feat: expose --all-projects flag via webhook args
abhijith-darshan May 27, 2026
6831b0d
test: update NewDesignateProvider call with allProjects parameter
abhijith-darshan May 27, 2026
a3a65bf
build(deps): bump gophercloud/v2 from v2.12.0 to v2.13.0
abhijith-darshan Jul 11, 2026
7770f0f
fix: remove context argument from openstack.NewDNSV2
abhijith-darshan Jul 11, 2026
1b7a3e2
feat: use recordsets.DeleteWithOpts for cross-project delete
abhijith-darshan Jul 11, 2026
58cf282
docs: mention admin token requirement in --all-projects flag description
abhijith-darshan Jul 14, 2026
8b598ed
Merge branch 'main' into feat/cross-project-zones
abhijith-darshan Jul 14, 2026
c762d82
feat: add webhook server and status server addr flag
abhijith-darshan Jul 15, 2026
bfee52c
feat: add integration test for --all-projects and --domain-filter
abhijith-darshan Jul 15, 2026
51e3f33
chore: shorten designate root_helper step name
abhijith-darshan Jul 15, 2026
248e6f8
Merge branch 'main' into feat/cross-project-zones
frittentheke Jul 31, 2026
a416a83
chore: tidy up
abhijith-darshan Aug 8, 2026
fb9b46d
ci: use separate zone for all-projects test and --status filter in wa…
abhijith-darshan Aug 8, 2026
c1346a5
Merge branch 'inovex:main' into feat/cross-project-zones
abhijith-darshan Aug 8, 2026
8a20f4a
ci: use --fqdn-template to generate all-projects.com hostnames
abhijith-darshan Aug 8, 2026
df2f913
(chore): bump gophercloud v2.14.0
abhijith-darshan Aug 21, 2026
e8b7664
chore: go mod tidy
abhijith-darshan Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions .github/workflows/devstack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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: |
Expand All @@ -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: |
Expand All @@ -107,3 +117,47 @@ 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 cloud_admin project and grant admin access
run: |
openstack --os-cloud devstack-admin project create cloud_admin
openstack --os-cloud devstack-admin role add --project cloud_admin --user admin admin
shell: bash

- name: Add devstack-cloud-admin cloud entry (admin user scoped to cloud_admin project)
run: |
AUTH_URL=$(python3 -c "import yaml; print(yaml.safe_load(open('/etc/openstack/clouds.yaml'))['clouds']['devstack-admin']['auth']['auth_url'])")
./devstack/tools/update_clouds_yaml.py \
--file /etc/openstack/clouds.yaml \
--os-cloud devstack-cloud-admin \
--os-region-name RegionOne \
--os-auth-url "$AUTH_URL" \
--os-username admin \
--os-password secret \
--os-project-name cloud_admin
shell: bash

- name: Create zone all-projects.com owned by demo project
run: |
openstack zone create --email admin@example.com all-projects.com.

- name: Wait for all-projects.com zone creation
run: |
while [ "$(openstack zone list -f value --status PENDING)" != "" ]; do date; openstack zone list -f value; sleep 1; done

- name: Start external-dns-openstack-webhook as cloud_admin with --all-projects on port 8889
run: |
OS_CLOUD=devstack-cloud-admin ./build/bin/external-dns-openstack-webhook --all-projects --domain-filter all-projects.com --webhook-server-address 127.0.0.1:8889 --status-server-address 0.0.0.0:8081 >/tmp/external-dns-openstack-webhook-all-projects.log 2>&1 &
for i in $(seq 30); do curl -sf http://127.0.0.1:8889 > /dev/null 2>&1 && break; sleep 1; done
curl -sf http://127.0.0.1:8889 > /dev/null || { cat /tmp/external-dns-openstack-webhook-all-projects.log; exit 1; }

- name: Run external-dns against all-projects.com as cloud_admin project user
run: ./build/external-dns --txt-owner-id all-projects-cluster-id --provider webhook --webhook-provider-url http://127.0.0.1:8889 --source fake --fqdn-template=all-projects.com --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.com
run: |
if [ $(openstack --os-cloud devstack-admin recordset list all-projects.com. --all-projects -f value | grep -c "all-projects-cluster-id") -ne 10 ]; then exit 1; fi
12 changes: 9 additions & 3 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
)

require (
github.com/gophercloud/gophercloud/v2 v2.13.0
github.com/gophercloud/gophercloud/v2 v2.14.0
github.com/gophercloud/utils/v2 v2.0.0-20260424064311-2eeed4ceb3e9
github.com/prometheus/client_golang v1.24.1
github.com/spf13/pflag v1.0.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/v
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gophercloud/gophercloud/v2 v2.13.0 h1:yEyJG+kABd8x2ttTqLsomihU6Kg2YheJSZhvP/QSx+8=
github.com/gophercloud/gophercloud/v2 v2.13.0/go.mod h1:KZRLVs6gcoy/pEFdkZqFjdYqnS0emMHv66UqdM5lMjU=
github.com/gophercloud/gophercloud/v2 v2.14.0 h1:xGxKCvyaOxJDc5FqrnKDNqtdYn43ocQPuJ2Cm4KX/cs=
github.com/gophercloud/gophercloud/v2 v2.14.0/go.mod h1:4fs5I9VH6Wg2LyocDL9xf0ASb8VD63tyLA8sgAX/69U=
github.com/gophercloud/utils/v2 v2.0.0-20260424064311-2eeed4ceb3e9 h1:WEPhYFzYmpfWHq+YPaP3+8pYf4wKQuJMkgPiiI4g7CY=
github.com/gophercloud/utils/v2 v2.0.0-20260424064311-2eeed4ceb3e9/go.mod h1:bIEH+wgvnxfegUewFuGi0u/L+ji5uiEuVVQMNKQASEY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down
15 changes: 9 additions & 6 deletions internal/designate/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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())
Expand Down
4 changes: 2 additions & 2 deletions internal/designate/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion internal/designate/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down