| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- package cmd
- import (
- "fmt"
- "strings"
- "github.com/spf13/cobra"
- "schlundtech-dns/internal/api"
- "schlundtech-dns/internal/output"
- )
- var recordsCmd = &cobra.Command{
- Use: "records",
- Short: "Read and change the records of a zone",
- Long: `Read and change the records of a zone.
- A record is addressed by its name relative to the zone: "@" is the zone apex,
- "www" is www.example.com. A name and type can hold several values.
- Setting a record replaces every value that name and type currently has, so
- there is nothing to delete first. Writes are public the moment they land, so
- run them with --dry-run first.`,
- Example: ` schlundtech-dns records list example.com
- schlundtech-dns records list example.com --type TXT`,
- }
- // recordView is the JSON shape of a record.
- type recordView struct {
- Name string `json:"name"`
- Type string `json:"type"`
- Value string `json:"value"`
- TTL int `json:"ttl"`
- }
- var recordsListCmd = &cobra.Command{
- Use: "list <zone>",
- Short: "Show the records of a zone",
- Long: `Show the records of a zone. Use --type to narrow the output to one record
- type, for example TXT, A or AAAA.
- This is read-only.`,
- Example: ` schlundtech-dns records list example.com
- schlundtech-dns records list example.com --type TXT
- schlundtech-dns records list example.com --output json`,
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- f, err := format()
- if err != nil {
- return err
- }
- recordType, _ := cmd.Flags().GetString("type")
- c, err := client()
- if err != nil {
- return err
- }
- records, err := c.Records(cmd.Context(), args[0], recordType)
- if err != nil {
- return err
- }
- views := make([]recordView, 0, len(records))
- rows := make([][]string, 0, len(records))
- for _, rr := range records {
- views = append(views, recordView{
- Name: displayName(rr.Name), Type: rr.Type, Value: rr.Value, TTL: rr.TTL,
- })
- rows = append(rows, []string{displayName(rr.Name), rr.Type, rr.Value, fmt.Sprint(rr.TTL)})
- }
- return output.Write(cmd.OutOrStdout(), f, []string{"NAME", "TYPE", "VALUE", "TTL"}, rows, views)
- },
- }
- var recordsSetCmd = &cobra.Command{
- Use: "set <zone>",
- Short: "Replace the records at one name and type",
- Long: `Replace every value of one record name and type.
- Pass --value more than once to set several values at the same name. Anything
- currently there is removed in the same task, so a rejected change leaves the
- zone untouched.
- --ttl defaults to the TTL the record already has. Use @ for the zone apex.`,
- Example: ` # preview first, then apply
- schlundtech-dns records set example.com --name @ --type A --value 203.0.113.10 --dry-run
- schlundtech-dns records set example.com --name @ --type A --value 203.0.113.10
- # several TXT values at once
- schlundtech-dns records set example.com --name @ --type TXT \
- --value 'v=spf1 -all' --value 'google-site-verification=abc'`,
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- zone := args[0]
- name, _ := cmd.Flags().GetString("name")
- recordType, _ := cmd.Flags().GetString("type")
- values, _ := cmd.Flags().GetStringArray("value")
- ttl, _ := cmd.Flags().GetInt("ttl")
- if err := requireType(recordType); err != nil {
- return err
- }
- if len(values) == 0 {
- return fmt.Errorf("--value is required")
- }
- c, err := client()
- if err != nil {
- return err
- }
- if flagDryRun {
- plan, err := c.PreviewSet(cmd.Context(), zone, name, recordType, values, ttl)
- if err != nil {
- return err
- }
- fmt.Fprint(cmd.OutOrStdout(), plan.String())
- fmt.Fprintln(cmd.OutOrStdout(), "dry run: nothing was sent to the gateway")
- return nil
- }
- if err := c.SetRecords(cmd.Context(), zone, name, recordType, values, ttl); err != nil {
- return err
- }
- fmt.Fprintf(cmd.OutOrStdout(), "set %s %s at %s in zone %s\n",
- recordType, joinValues(values), displayName(name), zone)
- return nil
- },
- }
- var recordsDeleteCmd = &cobra.Command{
- Use: "delete <zone>",
- Short: "Remove records at one name and type",
- Long: `Remove records at one name and type.
- Without --value every record of that name and type goes. With one or more
- --value only those are removed.`,
- Example: ` schlundtech-dns records delete example.com --name @ --type TXT --value 'v=spf1 -all' --dry-run
- schlundtech-dns records delete example.com --name @ --type TXT --value 'v=spf1 -all'`,
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- zone := args[0]
- name, _ := cmd.Flags().GetString("name")
- recordType, _ := cmd.Flags().GetString("type")
- values, _ := cmd.Flags().GetStringArray("value")
- if err := requireType(recordType); err != nil {
- return err
- }
- c, err := client()
- if err != nil {
- return err
- }
- if flagDryRun {
- records, err := c.Records(cmd.Context(), zone, recordType)
- if err != nil {
- return err
- }
- target, err := matchRecords(records, name, recordType, values)
- if err != nil {
- return fmt.Errorf("nothing to delete: %w", err)
- }
- out := cmd.OutOrStdout()
- for _, rr := range target {
- fmt.Fprintf(out, "would remove %s %s at %s in zone %s\n",
- rr.Type, rr.Value, displayName(rr.Name), zone)
- }
- fmt.Fprintln(out, "dry run: nothing was sent to the gateway")
- return nil
- }
- if err := c.DeleteRecords(cmd.Context(), zone, name, recordType, values); err != nil {
- return err
- }
- fmt.Fprintf(cmd.OutOrStdout(), "removed %s %s from %s in zone %s\n",
- recordType, joinValues(values), displayName(name), zone)
- return nil
- },
- }
- // requireType rejects an empty --type, which would otherwise be sent to the
- // gateway as a blank record type.
- func requireType(recordType string) error {
- if strings.TrimSpace(recordType) == "" {
- return fmt.Errorf("--type is required, for example TXT, A or AAAA")
- }
- return nil
- }
- // displayName renders the gateway's empty apex name for humans.
- func displayName(name string) string {
- if name == "" {
- return api.Apex
- }
- return name
- }
- // joinValues summarises a value list for a confirmation line.
- func joinValues(values []string) string {
- if len(values) == 0 {
- return "(all)"
- }
- return strings.Join(values, ", ")
- }
- // matchRecords picks the records a delete would touch, so a dry run shows the
- // same set the real command removes.
- func matchRecords(records []api.Record, name, recordType string, values []string) ([]api.Record, error) {
- name = strings.TrimSuffix(name, ".")
- if name == api.Apex {
- name = ""
- }
- wanted := map[string]bool{}
- for _, v := range values {
- wanted[v] = true
- }
- var out []api.Record
- for _, rr := range records {
- if !strings.EqualFold(rr.Type, recordType) || rr.Name != name {
- continue
- }
- if len(wanted) > 0 && !wanted[rr.Value] {
- continue
- }
- out = append(out, rr)
- }
- if len(out) == 0 {
- if len(values) == 0 {
- return nil, fmt.Errorf("no %s record at %s", recordType, displayName(name))
- }
- return nil, fmt.Errorf("none of the given values is a %s record at %s", recordType, displayName(name))
- }
- return out, nil
- }
- func init() {
- recordsListCmd.Flags().String("type", "", "only show records of this type, for example TXT")
- recordsSetCmd.Flags().String("name", api.Apex, `record name relative to the zone; "@" is the zone apex`)
- recordsSetCmd.Flags().String("type", "", "record type, for example TXT, A or AAAA")
- recordsSetCmd.Flags().StringArray("value", nil, "record value; repeat for several values at the same name")
- recordsSetCmd.Flags().Int("ttl", 0, "TTL in seconds; defaults to the current TTL")
- recordsDeleteCmd.Flags().String("name", api.Apex, `record name relative to the zone; "@" is the zone apex`)
- recordsDeleteCmd.Flags().String("type", "", "record type, for example TXT, A or AAAA")
- recordsDeleteCmd.Flags().StringArray("value", nil, "only remove this value; repeat for several")
- recordsCmd.AddCommand(recordsListCmd, recordsSetCmd, recordsDeleteCmd)
- rootCmd.AddCommand(recordsCmd)
- }
|