Sitelet https://github.com/k-orc/openstack-resource-controller/pull/955
Skip to content

Add DNSZone, DNSZoneShare, and RecordSet controllers for Designate support - #955

Closed
zunken1337 wants to merge 6 commits into
k-orc:mainfrom
zunken1337:new-controller-dnszone-share-recordset
Closed

zunken1337 wants to merge 6 commits into
k-orc:mainfrom
zunken1337:new-controller-dnszone-share-recordset

Conversation

@zunken1337

@zunken1337 zunken1337 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #954. Adds Designate DNS support: DNSZone (zone CRUD, with the projectID-in-status gap
from #825's review fixed), a new DNSZoneShare resource for Designate's zone-sharing API, and
RecordSet for individual DNS records within a zone.

Design question for maintainers: Share vs Transfer

Designate has two separate mechanisms for giving another project access to a zone:

  • Zone Transfer (transfer_requests/transfer_accepts) - moves full ownership, needs two
    different projects' credentials in one flow (owner creates the zone + transfer request,
    recipient accepts). Same two-credential shape already raised in Enhancement: Glance image member sharing (grant + accept) #948 for Glance image sharing.
  • Zone Share (zones/{id}/shares) - grants another project co-management rights over a
    zone's recordsets while the original project keeps ownership. Single credential only.

DNSZoneShare implements Share, not Transfer, deliberately - it's dramatically simpler (one
credential, no accept-side resource needed, same shape as the RBACPolicy controller for Neutron
network sharing), and we think it's the more broadly useful mechanism to support first. We
confirmed Zone Share is a real, working API
on our own OpenStack deployment (a live
create/list/delete round-trip, not just reading the gophercloud bindings) before committing to
this design. Transfer could be a follow-up matching the #948 pattern if there's demand for it.

Architecture notes

  • All three resources share one osclients.DNSClient (the "one service, one client" convention
    NetworkClient already established for Neutron), rather than a dedicated client per kind.
  • DNSZoneShare and RecordSet are both zone-scoped sub-resources in Designate's own API
    (Get/Create/Delete/Update all require the zone ID alongside the resource's own ID),
    which doesn't fit the generic actuator interfaces' single-ID GetOSResourceByID signature
    directly. Resolved by having each actuator resolve its owning zone once at construction time
    and reuse that across every method call on the same instance, rather than re-resolving per call.
  • DNSZoneShare has no update operation at all in Designate's real API (confirmed via
    gophercloud - only List/Get/Share/Unshare) - both zoneRef and targetProjectID are
    immutable here, unlike RBACPolicy's analogous targetProjectID field.
  • DNSZone.spec.resource.name (and RecordSet's) must be explicit and end with a period -
    Designate rejects anything else, and unlike most other resources there's no sensible
    object-name fallback (the ORC object's own Kubernetes name won't have a trailing period).
  • Because every Designate zone-share/recordset operation is zone-scoped, importing either kind
    needs a zone reference even when unmanaged (no spec.resource to read zoneRef from) - both
    DNSZoneShareFilter and RecordSetFilter gained a required zoneRef field, resolved via the
    same Dependency/*ImportDependency split ApplicationCredential already uses for
    userRef/userImportDependency. One known gap flagged here rather than silently worked around:
    import.id (a bare OpenStack ID, no filter) isn't supported for DNSZoneShare or
    RecordSet
    - there's no field anywhere to carry a zone reference in that mode, and Designate
    has no zone-independent way to look up either resource by ID alone. newActuator returns a
    clear Terminal error for this case rather than hanging; happy to hear design ideas if this
    gap matters to someone.

Testing

  • Unit tests for the actual mutable-field update paths (DNSZone: description/ttl/masters;
    RecordSet: description/ttl/records - both compared as sets, not positionally).
  • Full test/apivalidations suite extended with immutability, required-field, and PRIMARY/
    SECONDARY cross-field CEL validation tests for all three resources.
  • KUTTL e2e tests for all required scenarios per resource (DNSZoneShare's update scenario
    deliberately omitted - it has zero mutable fields, and the project's own testing guide says the
    update test is only required "for resources that implement mutability").
  • Two Designate behaviors the e2e tests depend on were verified live against a real OpenStack
    deployment rather than assumed from documentation: target_project_id isn't validated against
    Keystone (any string accepted, confirmed via a real openstack zone share create call with a
    garbage value), and an exact-duplicate share on the same zone is rejected but differing by zone
    is fine.
  • Caught and fixed a real bug via the full go test ./... run (not just the package under active
    work): a curly/smart quote had snuck into one CEL rule's string literal, which would have made
    the CRD uninstallable on any real Kubernetes API server (controller-gen doesn't validate CEL
    syntax, it just transcribes the rule string - the breakage only surfaced when envtest tried to
    actually install the CRD).
  • This is the first K-ORC controller to need Designate, so the e2e workflow's devstack deployment
    never had it enabled - added enable_plugin designate ... + DESIGNATE_BACKEND_DRIVER=bind9
    to .github/workflows/e2e.yaml, same pattern already used for neutron/manila.

Test plan

  • go build ./..., go vet ./..., gofmt -l . clean
  • go test ./... passes (only unrelated test/e2e, which needs a live kind cluster, fails in
    this environment)
  • make generate output committed (CRDs, deepcopy, adapters/controllers,
    applyconfiguration/clientset/lister/informer, mocks, API reference docs)
  • KUTTL e2e suite run against a real OpenStack cloud in CI

🤖 Generated with Claude Code

Joakim Johansson and others added 2 commits October 1, 2026 14:19
…pport

Closes the Designate gap tracked in k-orc#954: DNSZone/RecordSet CRUD, plus a new
DNSZoneShare resource for Designate's zone-sharing API (grants another
project co-management rights over a zone's recordsets without transferring
ownership - confirmed live against a real OpenStack deployment via a
create/list/delete round-trip before committing to this design over the
alternative zone-transfer mechanism, which needs two different projects'
credentials in one flow).

All three Designate resources share one osclients.DNSClient (the same
"one service, one client" convention NetworkClient already established for
Neutron), rather than a dedicated client per kind.

DNSZoneShare and RecordSet are both zone-scoped sub-resources in Designate's
own API (Get/Create/Delete/Update all require the zone ID alongside the
resource's own ID), which doesn't fit the generic actuator interfaces'
single-ID GetOSResourceByID signature directly - resolved by having each
actuator resolve its owning zone once at construction time and reuse that
across every method call on the same instance, rather than re-resolving per
call.

DNSZoneShare has no update operation at all in Designate's real API
(confirmed via gophercloud - only List/Get/Share/Unshare) - both zoneRef and
targetProjectID are immutable here, unlike RBACPolicy's analogous field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Joakim Johansson <joakimjohansson@cleura.nu>
Corrects the 16 scaffolded KUTTL scenario directories (DNSZone,
DNSZoneShare, RecordSet) from the scaffold tool's generic Name/Description
assumptions to the real schema - DNSZone requires an explicit, period-
terminated name (Designate rejects anything else, and there's no object-
name fallback like most other resources have); DNSZoneShare/RecordSet
dependencies all need a valid DNSZone with email set; DNSZoneShare's filter
only has targetProjectID (no name/description), same import/import-error
restructuring already done for RBACPolicy's analogous fields. Removed the
dnszoneshare-update scenario entirely - DNSZoneShare has zero mutable
fields, and K-ORC's own testing guide says the update test is only required
"for resources that implement mutability".

Also found and fixed a real bug while running the full suite one more time:
dnszone_types.go's PRIMARY-zone-requires-email CEL rule had a curly/smart
quote (U+201D) instead of a straight one in `self.email != ""`, breaking
CEL compilation - confirmed this would have made the CRD uninstallable on
any real Kubernetes API server, not just envtest, since controller-gen
transcribes the rule string as-is without validating CEL syntax itself.
Caught by the floatingip/image/router/etc. controller suites' own envtest
setup (which installs every CRD, not just DNSZone's), not by DNSZone's own
tests - a reminder that the full `go test ./...` run matters, not just the
package under active work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Joakim Johansson <joakimjohansson@cleura.nu>
@github-actions github-actions Bot added the semver:major Breaking change label Oct 1, 2026
Joakim Johansson and others added 3 commits October 1, 2026 15:55
- dnszone_types.go: rewrite the PRIMARY-requires-email CEL rule to use
  has(self.email) instead of comparing against an empty string literal.
  Go's gofmt doc-comment formatter collapses an empty '' pair into a
  single curly quote, corrupting the CEL expression; avoiding the
  empty-string comparison (matching every other CEL rule in this repo,
  which already prefers has()) sidesteps it entirely. Add MinLength:=1
  to Email so an explicit empty string still can't satisfy "has".
- dnszone_types.go, recordset_types.go: add the kube-api-linter-required
  +kubebuilder:validation:items:MaxLength marker to the Masters/Records
  status array fields (same pattern as Subnet's DNSNameservers).
- .golangci.yml: scope an exclusion for DNSZoneShare.TargetProjectID's
  noopenstackidref finding, same rationale and precedent as
  RBACPolicy.TargetProjectID (PR k-orc#953) - it's a raw OpenStack project ID
  by design, not a reference to an ORC Project object.
- config/manifests/bases/orc.clusterserviceversion.yaml: add the missing
  DNSZone/DNSZoneShare/RecordSet CSV entries that `make generate-bundle`
  needs committed (was causing the generate workflow's git diff check to
  fail).
- Regenerate CRD YAMLs and crd-reference.md to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Joakim Johansson <joakimjohansson@cleura.nu>
All 16 DNSZone/DNSZoneShare/RecordSet KUTTL scenarios failed in CI
(all 3 devstack flavors) with "failed to create dns service client:
No suitable endpoint could be found in the service catalog" - the e2e
workflow's devstack deployment never enabled Designate, so there's no
DNS service in the catalog for the new controllers to talk to. Add the
same enable_plugin pattern already used for neutron/manila, with
DESIGNATE_BACKEND_DRIVER=bind9 (devstack's own documented default/
lightest-weight backend, sufficient for zone/recordset CRUD against a
real API - no external DNS infrastructure needed for these tests).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Joakim Johansson <joakimjohansson@cleura.nu>
All 5 import/import-error/dependency-adjacent KUTTL scenarios that
remained failing after enabling Designate in CI shared one root cause:
newActuator() unconditionally resolved the owning zone via
zoneDependency, which only reads spec.resource.zoneRef. An unmanaged
(import) object never has spec.resource - only spec.import.filter -
so the zone was never resolved, RequireDependency degenerated into
"RequireDependencies returned empty depsMap, progressStatus, and
error", and every import/import-error/dependency scenario for both
kinds timed out waiting for a status that was never going to appear.

Fix: newActuator now branches on which half of the object is
populated, resolving the zone from spec.resource.zoneRef when managed
and from the new spec.import.filter.zoneRef when unmanaged (same
dual-dependency pattern ApplicationCredential already uses for
userRef/userImportDependency). Both DNSZoneShareFilter and
RecordSetFilter gained a required zoneRef field - Designate has no
"list across all zones" endpoint for either shares or recordsets, so
there was never a way to import one without already knowing its zone.
Bare import.id (no filter) has no field to carry a zone reference at
all; rather than leave it to hang, newActuator now returns a clear
Terminal error for that case, called out in the PR description for
maintainer awareness.

While fixing this, found and fixed a real, separate bug the previous
(never-triggered-by-tests) code masked: DNSZoneShare's
ListOSResourcesForImport/ListOSResourcesForAdoption listed every share
under a zone but never actually filtered by targetProjectID, despite
a comment claiming "the generic reconciler's own filter-matching"
would do it (it doesn't - every other actuator in this codebase does
its own client-side filtering, same as applicationcredential's
pattern this now follows). A zone with shares to two different target
projects would previously have an even chance of importing/adopting
the wrong one. dnszoneshare-import-error's KUTTL scenario now exercises
this directly: two shares in the *same* zone, an ambiguous zoneRef-only
filter, asserting the "more than one match" error.

Verified via `make generate`, `make lint` (0 issues), and `make test`
(full envtest/CEL suite + unit tests, 529/529 apivalidations specs,
zero regressions) on admin3 before pushing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Joakim Johansson <joakimjohansson@cleura.nu>
@mandre

mandre commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Thanks a lot for all the contributions the last couple of days, this is really welcome. Just know that it may take some time before we can give the proper attention to each one of them. I'll try my best to provide feedback soon. That said, this PR in particular would probably benefit from being split in 3 separate PRs that we'll merge in order (+16k LOC is way too big to review).

Two unrelated, genuine test-fixture bugs found via this round's e2e
failures (the code itself was fine - the controller correctly
reported these as Designate 400s / a stable waiting state):

- recordset-dependency and recordset-import used a RecordSet name
  that wasn't actually a subdomain of its own zoneRef's zone (e.g.
  "www.recordset-dependency-no-dnszone.example.com." under zone
  "recordset-dependency-pending.example.com." - a sibling domain, not
  a child of it). Designate correctly rejects this with 400
  "invalid_recordset_location: RecordSet is not contained within its
  parent zone". recordset-create-minimal/-full/-update already got
  this right (name "www.<zone-name>"); import/dependency didn't.
- dnszoneshare-import's 00-assert.yaml still expected the old "Waiting
  for OpenStack resource to be created externally" message, left over
  from before the previous commit moved the referenced zone's creation
  into step 01 (so the import object could spend a step genuinely
  waiting on its zoneRef dependency). At step 00 the zone doesn't
  exist yet, so the real - and stable - message is "Waiting for
  DNSZone/dnszoneshare-import to be created"; the stale assert text
  never matched, so the test never progressed past step 00 at all.

(gazpacho's one-off recordset-create-full failure in the same run
reached Available: True/Success by the time logs were collected -
a timing flake under concurrent load, not a real issue.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Joakim Johansson <joakimjohansson@cleura.nu>
@zunken1337

Copy link
Copy Markdown
Contributor Author

Thanks for the guidance - split into three stacked PRs as suggested:

Closing this one in favor of those three.

@zunken1337 zunken1337 closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver:major Breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Designate DNS zone + record support (DNSZone, DNSZoneShare, RecordSet)

2 participants