record.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package api
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. )
  7. // equalFold compares two record types case-insensitively.
  8. func equalFold(a, b string) bool { return strings.EqualFold(a, b) }
  9. // Apex is how a zone's own name is written on the command line. The gateway
  10. // represents it as the empty record name.
  11. const Apex = "@"
  12. // normalizeName maps a user-facing record name to the gateway's form. "@" and
  13. // "" both mean the zone apex.
  14. func normalizeName(name string) string {
  15. if name == Apex {
  16. return ""
  17. }
  18. return strings.TrimSuffix(name, ".")
  19. }
  20. // SetRecords replaces every value of one name and type with the given values,
  21. // in a single task: the existing records are removed and the new ones added
  22. // atomically, so a rejected task leaves the zone untouched.
  23. //
  24. // An empty zone is looked up to learn system_ns, which the update task
  25. // requires, and to read the current records so their TTL is preserved unless
  26. // ttl is non-zero.
  27. func (c *Client) SetRecords(ctx context.Context, zone, name, recordType string, values []string, ttl int) error {
  28. if len(values) == 0 {
  29. return fmt.Errorf("no value given: nothing to set")
  30. }
  31. name = normalizeName(name)
  32. z, err := c.Zone(ctx, zone)
  33. if err != nil {
  34. return err
  35. }
  36. if z.SystemNS == "" {
  37. return fmt.Errorf("zone %s came back without a system_ns, cannot update it", zone)
  38. }
  39. update := &Update{}
  40. for _, rr := range z.RRs {
  41. if rr.Name == name && equalFold(rr.Type, recordType) {
  42. update.RRRem = append(update.RRRem, Record{
  43. Name: rr.Name, Type: rr.Type, Value: rr.Value, TTL: rr.TTL,
  44. })
  45. if ttl == 0 {
  46. ttl = rr.TTL
  47. }
  48. }
  49. }
  50. for _, v := range values {
  51. update.RRAdd = append(update.RRAdd, Record{
  52. Name: name, Type: strings.ToUpper(recordType), Value: v, TTL: ttl,
  53. })
  54. }
  55. return c.applyUpdate(ctx, zone, z.SystemNS, update)
  56. }
  57. // DeleteRecords removes records of one name and type. With no values every
  58. // record of that name and type goes; with values only those are removed.
  59. func (c *Client) DeleteRecords(ctx context.Context, zone, name, recordType string, values []string) error {
  60. name = normalizeName(name)
  61. z, err := c.Zone(ctx, zone)
  62. if err != nil {
  63. return err
  64. }
  65. if z.SystemNS == "" {
  66. return fmt.Errorf("zone %s came back without a system_ns, cannot update it", zone)
  67. }
  68. wanted := map[string]bool{}
  69. for _, v := range values {
  70. wanted[v] = true
  71. }
  72. update := &Update{}
  73. matched := false
  74. for _, rr := range z.RRs {
  75. if rr.Name != name || !equalFold(rr.Type, recordType) {
  76. continue
  77. }
  78. if len(wanted) > 0 && !wanted[rr.Value] {
  79. continue
  80. }
  81. matched = true
  82. update.RRRem = append(update.RRRem, Record{
  83. Name: rr.Name, Type: rr.Type, Value: rr.Value, TTL: rr.TTL,
  84. })
  85. }
  86. if !matched {
  87. if len(values) == 0 {
  88. return fmt.Errorf("no %s record named %q in zone %s", recordType, displayName(name), zone)
  89. }
  90. return fmt.Errorf("none of the given values is a %s record named %q in zone %s", recordType, displayName(name), zone)
  91. }
  92. return c.applyUpdate(ctx, zone, z.SystemNS, update)
  93. }
  94. // applyUpdate sends one zone update task. The gateway answers with one msg per
  95. // zone; any error msg becomes an *APIError.
  96. func (c *Client) applyUpdate(ctx context.Context, zone, systemNS string, update *Update) error {
  97. if len(update.RRAdd) == 0 && len(update.RRRem) == 0 {
  98. return fmt.Errorf("nothing to change in zone %s", zone)
  99. }
  100. task := Task{
  101. Code: TaskZoneUpdateBulk,
  102. Default: update,
  103. Zone: &ZoneRef{Name: zone, SystemNS: systemNS},
  104. }
  105. _, _, err := c.run(ctx, task)
  106. return err
  107. }
  108. // displayName renders a gateway record name back for humans.
  109. func displayName(name string) string {
  110. if name == "" {
  111. return Apex
  112. }
  113. return name
  114. }
  115. // PlanSet describes what SetRecords would change, for --dry-run.
  116. type PlanSet struct {
  117. Zone string
  118. Name string
  119. Type string
  120. Remove []Record
  121. Add []Record
  122. }
  123. // String renders a plan as one line per change.
  124. func (p PlanSet) String() string {
  125. var b strings.Builder
  126. fmt.Fprintf(&b, "zone %s: set %d %s record(s) at %s\n", p.Zone, len(p.Add), p.Type, displayName(p.Name))
  127. for _, rr := range p.Remove {
  128. fmt.Fprintf(&b, " - %s %s (ttl %d)\n", rr.Type, rr.Value, rr.TTL)
  129. }
  130. for _, rr := range p.Add {
  131. fmt.Fprintf(&b, " + %s %s (ttl %d)\n", rr.Type, rr.Value, rr.TTL)
  132. }
  133. return b.String()
  134. }
  135. // PreviewSet computes what SetRecords would do without contacting the update
  136. // endpoint. It reads the zone, so it needs working credentials.
  137. func (c *Client) PreviewSet(ctx context.Context, zone, name, recordType string, values []string, ttl int) (PlanSet, error) {
  138. name = normalizeName(name)
  139. plan := PlanSet{Zone: zone, Name: name, Type: recordType}
  140. z, err := c.Zone(ctx, zone)
  141. if err != nil {
  142. return plan, err
  143. }
  144. for _, rr := range z.RRs {
  145. if rr.Name == name && equalFold(rr.Type, recordType) {
  146. plan.Remove = append(plan.Remove, rr)
  147. if ttl == 0 {
  148. ttl = rr.TTL
  149. }
  150. }
  151. }
  152. for _, v := range values {
  153. plan.Add = append(plan.Add, Record{
  154. Name: name, Type: strings.ToUpper(recordType), Value: v, TTL: ttl,
  155. })
  156. }
  157. return plan, nil
  158. }