| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- 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
- }
|