package api import ( "context" "fmt" "strings" ) // equalFold compares two record types case-insensitively. func equalFold(a, b string) bool { return strings.EqualFold(a, b) } // Apex is how a zone's own name is written on the command line. The gateway // represents it as the empty record name. const Apex = "@" // normalizeName maps a user-facing record name to the gateway's form. "@" and // "" both mean the zone apex. func normalizeName(name string) string { if name == Apex { return "" } return strings.TrimSuffix(name, ".") } // SetRecords replaces every value of one name and type with the given values, // in a single task: the existing records are removed and the new ones added // atomically, so a rejected task leaves the zone untouched. // // An empty zone is looked up to learn system_ns, which the update task // requires, and to read the current records so their TTL is preserved unless // ttl is non-zero. func (c *Client) SetRecords(ctx context.Context, zone, name, recordType string, values []string, ttl int) error { if len(values) == 0 { return fmt.Errorf("no value given: nothing to set") } name = normalizeName(name) z, err := c.Zone(ctx, zone) if err != nil { return err } if z.SystemNS == "" { return fmt.Errorf("zone %s came back without a system_ns, cannot update it", zone) } update := &Update{} for _, rr := range z.RRs { if rr.Name == name && equalFold(rr.Type, recordType) { update.RRRem = append(update.RRRem, Record{ Name: rr.Name, Type: rr.Type, Value: rr.Value, TTL: rr.TTL, }) if ttl == 0 { ttl = rr.TTL } } } for _, v := range values { update.RRAdd = append(update.RRAdd, Record{ Name: name, Type: strings.ToUpper(recordType), Value: v, TTL: ttl, }) } return c.applyUpdate(ctx, zone, z.SystemNS, update) } // DeleteRecords removes records of one name and type. With no values every // record of that name and type goes; with values only those are removed. func (c *Client) DeleteRecords(ctx context.Context, zone, name, recordType string, values []string) error { name = normalizeName(name) z, err := c.Zone(ctx, zone) if err != nil { return err } if z.SystemNS == "" { return fmt.Errorf("zone %s came back without a system_ns, cannot update it", zone) } wanted := map[string]bool{} for _, v := range values { wanted[v] = true } update := &Update{} matched := false for _, rr := range z.RRs { if rr.Name != name || !equalFold(rr.Type, recordType) { continue } if len(wanted) > 0 && !wanted[rr.Value] { continue } matched = true update.RRRem = append(update.RRRem, Record{ Name: rr.Name, Type: rr.Type, Value: rr.Value, TTL: rr.TTL, }) } if !matched { if len(values) == 0 { return fmt.Errorf("no %s record named %q in zone %s", recordType, displayName(name), zone) } return fmt.Errorf("none of the given values is a %s record named %q in zone %s", recordType, displayName(name), zone) } return c.applyUpdate(ctx, zone, z.SystemNS, update) } // applyUpdate sends one zone update task. The gateway answers with one msg per // zone; any error msg becomes an *APIError. func (c *Client) applyUpdate(ctx context.Context, zone, systemNS string, update *Update) error { if len(update.RRAdd) == 0 && len(update.RRRem) == 0 { return fmt.Errorf("nothing to change in zone %s", zone) } task := Task{ Code: TaskZoneUpdateBulk, Default: update, Zone: &ZoneRef{Name: zone, SystemNS: systemNS}, } _, _, err := c.run(ctx, task) return err } // displayName renders a gateway record name back for humans. func displayName(name string) string { if name == "" { return Apex } return name } // PlanSet describes what SetRecords would change, for --dry-run. type PlanSet struct { Zone string Name string Type string Remove []Record Add []Record } // String renders a plan as one line per change. func (p PlanSet) String() string { var b strings.Builder fmt.Fprintf(&b, "zone %s: set %d %s record(s) at %s\n", p.Zone, len(p.Add), p.Type, displayName(p.Name)) for _, rr := range p.Remove { fmt.Fprintf(&b, " - %s %s (ttl %d)\n", rr.Type, rr.Value, rr.TTL) } for _, rr := range p.Add { fmt.Fprintf(&b, " + %s %s (ttl %d)\n", rr.Type, rr.Value, rr.TTL) } return b.String() } // PreviewSet computes what SetRecords would do without contacting the update // endpoint. It reads the zone, so it needs working credentials. func (c *Client) PreviewSet(ctx context.Context, zone, name, recordType string, values []string, ttl int) (PlanSet, error) { name = normalizeName(name) plan := PlanSet{Zone: zone, Name: name, Type: recordType} z, err := c.Zone(ctx, zone) if err != nil { return plan, err } for _, rr := range z.RRs { if rr.Name == name && equalFold(rr.Type, recordType) { plan.Remove = append(plan.Remove, rr) if ttl == 0 { ttl = rr.TTL } } } for _, v := range values { plan.Add = append(plan.Add, Record{ Name: name, Type: strings.ToUpper(recordType), Value: v, TTL: ttl, }) } return plan, nil }