Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions docs/cli/datumctl-dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,19 @@ Drop `--dry-run` to apply the changes. Re-running an unchanged file reports `No

Datum manages some records for you — the zone's SOA and apex NS records, and records created by a Gateway. `apply` never prunes or modifies them, and reports what it skipped.

## Private zones that share a domain

Private zones in different VPCs can use the same domain. Every VPC's default zone is `datum.internal`, for example. When more than one zone matches what you typed, the plugin refuses rather than guess, and lists each zone's object name and the VPCs that use it.

Pick one with `--vpc <name>`, which uses the zone that VPC resolves, or pass the zone's object name in place of the domain:

```sh
datumctl dns record create datum.internal db A 10.0.0.5 --vpc app
datumctl dns record list managed-3f9c2a
```

`--vpc` works on every `record` command and on `zone describe`, `delete`, `nameservers`, `import`, and `export`. It applies only to private zones. Suggested next steps name a private zone by its object name, and deleting a private zone asks you to confirm with its object name.

## Output formats and scripting

Every command accepts `-o`:
Expand Down Expand Up @@ -352,6 +365,7 @@ Add `--verbose` to see the underlying cause.
| DNS is not enabled for the project | Run `datumctl services enable dns.networking.miloapis.com --wait`. |
| The domain doesn't resolve | Run `datumctl dns zone nameservers <domain> --check`. If delegation is `Incomplete` or `Partial`, fix it at your registrar. |
| A record is stuck at `Pending` | This is normal right after a write. If it lasts, run `datumctl dns record describe <domain> <name>` for the server's message. |
| `N zones in this project match "<domain>"` | Several zones share the domain. See [Private zones that share a domain](#private-zones-that-share-a-domain). |
| `Conflict` | Another record occupies that name. Usually the name isn't inside the zone — check for `www.example.com` where `www` was meant. Otherwise see [DNS record conflict troubleshooting](../troubleshooting/dnsrecordset-downstream-orphan.md). |
| `Not owner` | Another record set owns that name; `describe` names it. Edit the record through that set, or delete the set first. Records Datum manages, such as a Gateway's, revert if you edit them. |

Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/dns/record/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func runApply(cmd *cobra.Command, domain, file string, prune, dryRun bool) error
return err
}

zone, err := resolveZone(ctx, c, domain)
zone, err := resolveZone(ctx, c, domain, util.VPCFromCmd(cmd))
if err != nil {
return err
}
Expand Down Expand Up @@ -171,7 +171,7 @@ func confirmApply(cmd *cobra.Command, zone *dnsv1alpha1.DNSZone, plan *plan, err
}
ok, err := util.ConfirmYesNo(cmd.InOrStdin(), errOut,
fmt.Sprintf("Apply %d %s to %s?",
len(plan.changes), pluralize(len(plan.changes), "change"), zone.Spec.DomainName),
len(plan.changes), pluralize(len(plan.changes), "change"), util.ZoneLabel(zone)),
false)
if err != nil {
return err
Expand Down
56 changes: 43 additions & 13 deletions internal/cmd/dns/record/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ const (
// can inject a fake client without a control plane behind it.
var clientFactory = util.NewClient

// resolveZone finds the DNSZone serving a domain. Users type the domain name,
// which is a selectable field; the object name is accepted as a fallback so a
// name copied out of `kubectl get dnszones` also works.
func resolveZone(ctx context.Context, c client.Client, domain string) (*dnsv1alpha1.DNSZone, error) {
// resolveZone resolves what a user typed, a domain or an object name, to one
// DNSZone.
func resolveZone(ctx context.Context, c client.Client, domain, vpc string) (*dnsv1alpha1.DNSZone, error) {
name := normalizeDomain(domain)
if name == "" {
return nil, util.UsageErrorf("a zone domain is required").
Expand All @@ -48,23 +47,54 @@ func resolveZone(ctx context.Context, c client.Client, domain string) (*dnsv1alp
return nil, util.ClassifyError(fmt.Errorf("listing zones: %w", err))
}

if len(list.Items) > 0 {
sort.Slice(list.Items, func(i, j int) bool { return list.Items[i].Name < list.Items[j].Name })
return &list.Items[0], nil
}

var byName dnsv1alpha1.DNSZone
err := c.Get(ctx, types.NamespacedName{Namespace: util.ResourceNamespace, Name: name}, &byName)
switch {
case err == nil:
return &byName, nil
case apierrors.IsNotFound(err):
if !containsZone(list.Items, byName.Name) {
list.Items = append(list.Items, byName)
}
case !apierrors.IsNotFound(err):
return nil, util.ClassifyError(fmt.Errorf("getting zone: %w", err))
}

if len(list.Items) == 0 {
return nil, util.NewCLIError(util.ExitNotFound, fmt.Sprintf("zone %q not found", name)).
WithFix("run `datumctl dns zone list` to see the zones in this project.").
WithCause(err)
default:
return nil, util.ClassifyError(fmt.Errorf("getting zone: %w", err))
}
zone, err := util.PickZone(ctx, c, list.Items, name, vpc)
if err != nil {
return nil, err
}
sharing := 0
for i := range list.Items {
if list.Items[i].Spec.DomainName == zone.Spec.DomainName {
sharing++
}
}
if zone.Spec.DomainName != name {
var same dnsv1alpha1.DNSZoneList
if err := c.List(ctx, &same,
client.InNamespace(util.ResourceNamespace),
client.MatchingFields{fieldDomainName: zone.Spec.DomainName},
); err == nil {
sharing = len(same.Items)
}
}
if sharing > 1 {
util.MarkSharedDomain(zone)
}
return zone, nil
}

func containsZone(zones []dnsv1alpha1.DNSZone, name string) bool {
for i := range zones {
if zones[i].Name == name {
return true
}
}
return false
}

// normalizeDomain folds a domain to the spelling spec.domainName uses: lowercase
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/dns/record/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func runMutate(cmd *cobra.Command, args []string, opts *mutateOptions) error {
if err != nil {
return err
}
zone, err := resolveZone(ctx, c, args[0])
zone, err := resolveZone(ctx, c, args[0], util.VPCFromCmd(cmd))
if err != nil {
return err
}
Expand Down
12 changes: 6 additions & 6 deletions internal/cmd/dns/record/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func runDelete(cmd *cobra.Command, args []string, opts *deleteOptions) error {
if err != nil {
return err
}
zone, err := resolveZone(ctx, c, args[0])
zone, err := resolveZone(ctx, c, args[0], util.VPCFromCmd(cmd))
if err != nil {
return err
}
Expand All @@ -103,7 +103,7 @@ func runDelete(cmd *cobra.Command, args []string, opts *deleteOptions) error {
return err
}
if set == nil {
return notFoundError(t, ownerName, zoneDomain)
return notFoundError(t, ownerName, zoneDomain, util.ZoneArg(zone))
}
// What is being deleted is established before the managed-record guard, so
// a name or value that is simply not there says so rather than being told
Expand All @@ -114,9 +114,9 @@ func runDelete(cmd *cobra.Command, args []string, opts *deleteOptions) error {
return util.NewCLIError(util.ExitNotFound,
fmt.Sprintf("%s has no %s value %q", ownerDisplay(ownerName, zoneDomain), t, rdata.Render(t, *target))).
WithFix(fmt.Sprintf("list what is there: datumctl dns record list %s --name %s --type %s",
zoneDomain, displayName(ownerName), t))
util.ZoneArg(zone), displayName(ownerName), t))
}
return notFoundError(t, ownerName, zoneDomain)
return notFoundError(t, ownerName, zoneDomain, util.ZoneArg(zone))
}

if err := guardMutation(errOut, set, zone, t, ownerName, opts.force); err != nil {
Expand Down Expand Up @@ -198,8 +198,8 @@ func deletePrompt(doomed []dnsv1alpha1.RecordEntry, t dnsv1alpha1.RRType, ownerN
return fmt.Sprintf("Delete all %d %s records for %s?", len(doomed), t, who)
}

func notFoundError(t dnsv1alpha1.RRType, ownerName, zoneDomain string) error {
func notFoundError(t dnsv1alpha1.RRType, ownerName, zoneDomain, zoneArg string) error {
return util.NewCLIError(util.ExitNotFound,
fmt.Sprintf("no %s records for %s", t, ownerDisplay(ownerName, zoneDomain))).
WithFix(fmt.Sprintf("list what is there: datumctl dns record list %s", zoneDomain))
WithFix(fmt.Sprintf("list what is there: datumctl dns record list %s", zoneArg))
}
12 changes: 6 additions & 6 deletions internal/cmd/dns/record/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func runDescribe(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
zone, err := resolveZone(ctx, c, args[0])
zone, err := resolveZone(ctx, c, args[0], util.VPCFromCmd(cmd))
if err != nil {
return err
}
Expand All @@ -96,7 +96,7 @@ func runDescribe(cmd *cobra.Command, args []string) error {
}
}
if len(owning) == 0 {
return describeNotFound(ownerName, zoneDomain, rrTypes)
return describeNotFound(ownerName, zoneDomain, util.ZoneArg(zone), rrTypes)
}

switch format {
Expand Down Expand Up @@ -190,7 +190,7 @@ func managedBy(p provenance, rs *dnsv1alpha1.DNSRecordSet) string {
}

func printNextSteps(out io.Writer, zone *dnsv1alpha1.DNSZone, ownerName string, owning []dnsv1alpha1.DNSRecordSet) {
domain := zone.Spec.DomainName
domain := util.ZoneArg(zone)
name := displayName(ownerName)
t := string(owning[0].Spec.RecordType)

Expand All @@ -201,11 +201,11 @@ func printNextSteps(out io.Writer, zone *dnsv1alpha1.DNSZone, ownerName string,
_, _ = fmt.Fprintf(out, " See the whole zone: datumctl dns record list %s\n", domain)
}

func describeNotFound(ownerName, zoneDomain string, rrTypes []dnsv1alpha1.RRType) error {
func describeNotFound(ownerName, zoneDomain, zoneArg string, rrTypes []dnsv1alpha1.RRType) error {
if len(rrTypes) == 1 {
return notFoundError(rrTypes[0], ownerName, zoneDomain)
return notFoundError(rrTypes[0], ownerName, zoneDomain, zoneArg)
}
return util.NewCLIError(util.ExitNotFound,
fmt.Sprintf("no records for %s", ownerDisplay(ownerName, zoneDomain))).
WithFix(fmt.Sprintf("list what is there: datumctl dns record list %s", zoneDomain))
WithFix(fmt.Sprintf("list what is there: datumctl dns record list %s", zoneArg))
}
14 changes: 7 additions & 7 deletions internal/cmd/dns/record/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func runList(cmd *cobra.Command, domain string, opts *listOptions) error {
if err != nil {
return err
}
zone, err := resolveZone(ctx, c, domain)
zone, err := resolveZone(ctx, c, domain, util.VPCFromCmd(cmd))
if err != nil {
return err
}
Expand Down Expand Up @@ -202,7 +202,7 @@ func runList(cmd *cobra.Command, domain string, opts *listOptions) error {
}

if len(rows) == 0 {
printEmpty(out, zone.Spec.DomainName, opts, rrTypes, beforeRowFilters > 0)
printEmpty(out, zone.Spec.DomainName, util.ZoneArg(zone), opts, rrTypes, beforeRowFilters > 0)
return nil
}

Expand Down Expand Up @@ -468,7 +468,7 @@ func printNames(out io.Writer, rows []row) {
// different situation from a zone with nothing to show and wants the opposite
// advice: offering to populate a zone that already holds a thousand records
// reads as though they have been lost.
func printEmpty(out io.Writer, domain string, opts *listOptions, rrTypes []dnsv1alpha1.RRType, excludedByFilters bool) {
func printEmpty(out io.Writer, domain, zoneArg string, opts *listOptions, rrTypes []dnsv1alpha1.RRType, excludedByFilters bool) {
// The headline reports what was asked; the advice below reports what to do
// about it, and the two turn on different questions.
if listIsFiltered(opts) {
Expand All @@ -479,8 +479,8 @@ func printEmpty(out io.Writer, domain string, opts *listOptions, rrTypes []dnsv1

if excludedByFilters {
_, _ = fmt.Fprintf(out, "\nNext steps:\n")
_, _ = fmt.Fprintf(out, " Show every record: datumctl dns record list %s\n", domain)
_, _ = fmt.Fprintf(out, " Widen the filter: datumctl dns record list %s --help\n", domain)
_, _ = fmt.Fprintf(out, " Show every record: datumctl dns record list %s\n", zoneArg)
_, _ = fmt.Fprintf(out, " Widen the filter: datumctl dns record list %s --help\n", zoneArg)
return
}

Expand All @@ -491,8 +491,8 @@ func printEmpty(out io.Writer, domain string, opts *listOptions, rrTypes []dnsv1
}
}
_, _ = fmt.Fprintf(out, "\nGet started:\n")
_, _ = fmt.Fprintf(out, " Add a record: datumctl dns record create %s %s\n", domain, example)
_, _ = fmt.Fprintf(out, " Import a zone: datumctl dns zone import %s --file zone.txt\n", domain)
_, _ = fmt.Fprintf(out, " Add a record: datumctl dns record create %s %s\n", zoneArg, example)
_, _ = fmt.Fprintf(out, " Import a zone: datumctl dns zone import %s --file zone.txt\n", zoneArg)
}

// warnUnfilterable says so when a row filter was given alongside raw-object
Expand Down
4 changes: 4 additions & 0 deletions internal/cmd/dns/record/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package record

import (
"github.com/spf13/cobra"

"go.miloapis.com/dns-operator/internal/cmd/dns/util"
)

// Command builds the `dns record` noun and its verbs.
Expand Down Expand Up @@ -50,6 +52,8 @@ so a value pasted out of a provider export needs no translation.`,
},
}

util.AddVPCFlag(cmd.PersistentFlags())

cmd.AddCommand(
listCommand(),
createCommand(),
Expand Down
Loading
Loading