From cd79ec39611228f86aa7fc5126495f56935d168a Mon Sep 17 00:00:00 2001 From: Alexandru-Claudius Virtopeanu Date: Thu, 27 Jun 2024 16:46:36 +0300 Subject: [PATCH] feat: reverse dns (#448) * dep: update dns sdk * feat: add quota * feat: add dnsec * feat: dnssec * fix: cols for creation * doc: regen docs * fix: wiki dns link * feat: add reverse records * list reverse records * create reverse records * ip completions * delete * doc: regen docs: * fix: enabled correct jsonpath * feat: update * feat: reverse record get * doc: regen docs * doc: update changelog --- CHANGELOG.md | 5 +- commands/dns/dns.go | 2 + commands/dns/record/create.go | 2 +- commands/dns/reverse-record/create.go | 81 ++++++++++++ commands/dns/reverse-record/delete.go | 93 +++++++++++++ commands/dns/reverse-record/get.go | 69 ++++++++++ commands/dns/reverse-record/list.go | 59 +++++++++ commands/dns/reverse-record/reverse-record.go | 122 ++++++++++++++++++ commands/dns/reverse-record/update.go | 98 ++++++++++++++ docs/subcommands/DNS/record/create.md | 4 +- docs/subcommands/DNS/reverse/record/create.md | 54 ++++++++ docs/subcommands/DNS/reverse/record/delete.md | 55 ++++++++ docs/subcommands/DNS/reverse/record/get.md | 53 ++++++++ docs/subcommands/DNS/reverse/record/list.md | 54 ++++++++ docs/subcommands/DNS/reverse/record/update.md | 55 ++++++++ docs/summary.md | 7 + internal/constants/constants.go | 2 + internal/printer/json2table/jsonpaths/dns.go | 9 +- 18 files changed, 818 insertions(+), 6 deletions(-) create mode 100644 commands/dns/reverse-record/create.go create mode 100644 commands/dns/reverse-record/delete.go create mode 100644 commands/dns/reverse-record/get.go create mode 100644 commands/dns/reverse-record/list.go create mode 100644 commands/dns/reverse-record/reverse-record.go create mode 100644 commands/dns/reverse-record/update.go create mode 100644 docs/subcommands/DNS/reverse/record/create.md create mode 100644 docs/subcommands/DNS/reverse/record/delete.md create mode 100644 docs/subcommands/DNS/reverse/record/get.md create mode 100644 docs/subcommands/DNS/reverse/record/list.md create mode 100644 docs/subcommands/DNS/reverse/record/update.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f6745e9be..b0da1130a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ ### Added - Added support for DNS resources: - - `ionosctl dns keys` commands which allows you to enable/disable DNSSEC and manage DNSKEY records - - `ionosctl dns quota` commands which allows you to get the DNS quota for your account + - `ionosctl dns keys` commands which allows you to enable/disable DNSSEC and manage DNSKEY records. + - `ionosctl dns quota` commands which allows you to get the DNS quota for your account. + - `ionosctl dns reverse-record` commands which allows you to manage reverse DNS records. ### Fixed - Fixed the column path mapping for 'server' resource to display the actual server's type ('CUBE'/'ENTERPRISE'), diff --git a/commands/dns/dns.go b/commands/dns/dns.go index 345c2fbc3..083e61105 100644 --- a/commands/dns/dns.go +++ b/commands/dns/dns.go @@ -4,6 +4,7 @@ import ( "github.com/ionos-cloud/ionosctl/v6/commands/dns/dnssec" "github.com/ionos-cloud/ionosctl/v6/commands/dns/quota" "github.com/ionos-cloud/ionosctl/v6/commands/dns/record" + reverse_record "github.com/ionos-cloud/ionosctl/v6/commands/dns/reverse-record" "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/spf13/cobra" @@ -19,6 +20,7 @@ func DNSCommand() *core.Command { } cmd.AddCommand(zone.ZoneCommand()) cmd.AddCommand(record.RecordCommand()) + cmd.AddCommand(reverse_record.Root()) cmd.AddCommand(quota.Root()) cmd.AddCommand(dnssec.Root()) diff --git a/commands/dns/record/create.go b/commands/dns/record/create.go index 48f758c5d..04f270aa2 100644 --- a/commands/dns/record/create.go +++ b/commands/dns/record/create.go @@ -28,7 +28,7 @@ func ZonesRecordsPostCmd() *core.Command { Resource: "record", Verb: "create", Aliases: []string{"c", "post"}, - ShortDesc: "Create a record. Wiki: https://docs.ionos.com/dns-as-a-service/readme/api-how-tos/create-a-new-dns-record", + ShortDesc: "Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/api-how-tos/create-dns-record", Example: "ionosctl dns r create --zone foo-bar.com --type A --content 1.2.3.4 --name \\*", PreCmdRun: func(c *core.PreCommandConfig) error { if err := core.CheckRequiredFlags(c.Command, c.NS, constants.FlagName, constants.FlagZone, constants.FlagContent, constants.FlagType); err != nil { diff --git a/commands/dns/reverse-record/create.go b/commands/dns/reverse-record/create.go new file mode 100644 index 000000000..923682e43 --- /dev/null +++ b/commands/dns/reverse-record/create.go @@ -0,0 +1,81 @@ +package reverse_record + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/internal/client" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table/jsonpaths" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + "github.com/ionos-cloud/ionosctl/v6/pkg/pointer" + "github.com/ionos-cloud/ionosctl/v6/pkg/uuidgen" + dns "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func Create() *core.Command { + cmd := core.NewCommand(context.Background(), nil, core.CommandBuilder{ + Namespace: "dns", + Resource: "reverse-record", + Verb: "create", + Aliases: []string{"c", "post"}, + ShortDesc: "Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/api-how-tos/create-and-manage-reverse-dns", + Example: "ionosctl dns rr create --name mail.example.com --ip 5.6.7.8", + PreCmdRun: func(c *core.PreCommandConfig) error { + if err := core.CheckRequiredFlags(c.Command, c.NS, constants.FlagName, constants.FlagIp); err != nil { + return err + } + + return nil + }, + CmdRun: func(c *core.CommandConfig) error { + rec, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsPut(context.Background(), uuidgen.Must()). + ReverseRecordEnsure(dns.ReverseRecordEnsure{ + Properties: &dns.ReverseRecord{ + Name: pointer.From(viper.GetString(core.GetFlagName(c.NS, constants.FlagName))), + Ip: pointer.From(viper.GetString(core.GetFlagName(c.NS, constants.FlagIp))), + Description: pointer.From(viper.GetString(core.GetFlagName(c.NS, constants.FlagDescription))), + }, + }).Execute() + if err != nil { + return err + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsReverseRecord, rec, + tabheaders.GetHeadersAllDefault(allCols, cols)) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + InitClient: true, + }) + + cmd.AddStringFlag(constants.FlagName, constants.FlagNameShort, "", "The name of the DNS Reverse Record.", core.RequiredFlagOption()) + cmd.AddStringFlag(constants.FlagIp, "", "", "[IPv4/IPv6] Specifies for which IP address the reverse record should be created. The IP addresses needs to be owned by the contract", core.RequiredFlagOption()) + cmd.Command.RegisterFlagCompletionFunc(constants.FlagIp, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ipblocks, _, err := client.Must().CloudClient.IPBlocksApi.IpblocksGet(context.Background()).Execute() + if err != nil || ipblocks.Items == nil || len(*ipblocks.Items) == 0 { + return nil, cobra.ShellCompDirectiveError + } + var ips []string + for _, ipblock := range *ipblocks.Items { + if ipblock.Properties.Ips != nil { + ips = append(ips, *ipblock.Properties.Ips...) + } + } + return ips, cobra.ShellCompDirectiveNoFileComp + }) + + cmd.AddStringFlag(constants.FlagDescription, "", "", "Description stored along with the reverse DNS record to describe its usage") + cmd.Command.SilenceUsage = true + + return cmd +} diff --git a/commands/dns/reverse-record/delete.go b/commands/dns/reverse-record/delete.go new file mode 100644 index 000000000..b7218f027 --- /dev/null +++ b/commands/dns/reverse-record/delete.go @@ -0,0 +1,93 @@ +package reverse_record + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/internal/client" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/ionos-cloud/ionosctl/v6/pkg/functional" + ionoscloud "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func Delete() *core.Command { + cmd := core.NewCommand(context.Background(), nil, core.CommandBuilder{ + Namespace: "dns", + Resource: "reverse-record", + Verb: "delete", + Aliases: []string{"d", "del", "rm"}, + ShortDesc: "Delete a record", + Example: "ionosctl dns rr delete -af\n" + + "ionosctl dns rr delete --record RECORD_IP\n" + + "ionosctl dns rr delete --record RECORD_ID", + PreCmdRun: func(c *core.PreCommandConfig) error { + if err := core.CheckRequiredFlagsSets(c.Command, c.NS, + []string{constants.FlagRecord}, []string{constants.ArgAll}); err != nil { + return err + } + + return nil + }, + CmdRun: func(c *core.CommandConfig) error { + if all := viper.GetBool(core.GetFlagName(c.NS, constants.ArgAll)); all { + return deleteAll(c) + } + + return deleteSingle(c, viper.GetString(core.GetFlagName(c.NS, constants.FlagRecord))) + }, + InitClient: true, + }) + + cmd.AddStringFlag(constants.FlagRecord, "", "", "The record ID or IP which you want to delete", core.RequiredFlagOption()) + // Completions: all current IPs + cmd.Command.RegisterFlagCompletionFunc(constants.FlagRecord, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ips := RecordsProperty(func(read ionoscloud.ReverseRecordRead) string { + return *read.Properties.Ip + }) + return ips, cobra.ShellCompDirectiveNoFileComp + }) + cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, "Delete all records if set", core.RequiredFlagOption()) + + cmd.Command.SilenceUsage = true + return cmd +} + +func deleteAll(c *core.CommandConfig) error { + records, err := Records() + if err != nil { + return fmt.Errorf("failed getting all records: %w", err) + } + + return functional.ApplyAndAggregateErrors(*records.GetItems(), func(r ionoscloud.ReverseRecordRead) error { + return deleteSingle(c, *r.Id) + }) +} + +func deleteSingle(c *core.CommandConfig, ipOrIdOfRecord string) error { + id, err := Resolve(ipOrIdOfRecord) + if err != nil { + return fmt.Errorf("can't resolve IP %s to a record ID: %s", ipOrIdOfRecord, err) + } + + r, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsFindById(context.Background(), id).Execute() + if err != nil { + return fmt.Errorf("failed querying for reverse record ID %s: %s", id, err) + } + yes := confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf( + "Are you sure you want to delete record %s (IP: '%s'; description: '%s'; ID: '%s')", + *r.Properties.Name, *r.Properties.Ip, *r.Properties.Description, *r.Id), + viper.GetBool(constants.ArgForce)) + if !yes { + return fmt.Errorf("user cancelled deletion") + } + + _, _, err = client.Must().DnsClient.ReverseRecordsApi.ReverserecordsDelete(context.Background(), + *r.Id, + ).Execute() + + return err +} diff --git a/commands/dns/reverse-record/get.go b/commands/dns/reverse-record/get.go new file mode 100644 index 000000000..506ea2ee2 --- /dev/null +++ b/commands/dns/reverse-record/get.go @@ -0,0 +1,69 @@ +package reverse_record + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/internal/client" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table/jsonpaths" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + ionoscloud "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func Get() *core.Command { + cmd := core.NewCommand(context.Background(), nil, core.CommandBuilder{ + Namespace: "dns", + Resource: "reverse-record", + Verb: "get", + Aliases: []string{"g"}, + ShortDesc: "Find a record by IP or ID", + Example: "ionosctl dns rr get --record RECORD_IP\n" + + "ionosctl dns rr get --record RECORD_ID", + PreCmdRun: func(c *core.PreCommandConfig) error { + if err := core.CheckRequiredFlagsSets(c.Command, c.NS, + []string{constants.FlagRecord}, []string{constants.ArgAll}); err != nil { + return err + } + + return nil + }, + CmdRun: func(c *core.CommandConfig) error { + id, err := Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagRecord))) + if err != nil { + return fmt.Errorf("can't resolve IP to a record ID: %s", err) + } + + rec, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsFindById(context.Background(), id).Execute() + if err != nil { + return fmt.Errorf("failed querying for reverse record ID %s: %s", id, err) + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsReverseRecord, rec, + tabheaders.GetHeadersAllDefault(allCols, cols)) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + InitClient: true, + }) + + cmd.AddStringFlag(constants.FlagRecord, "", "", "The record ID or IP, for identifying which record you want to update", core.RequiredFlagOption()) + cmd.Command.RegisterFlagCompletionFunc(constants.FlagRecord, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ips := RecordsProperty(func(read ionoscloud.ReverseRecordRead) string { + return *read.Properties.Ip + }) + return ips, cobra.ShellCompDirectiveNoFileComp + }) + + cmd.Command.SilenceUsage = true + return cmd +} diff --git a/commands/dns/reverse-record/list.go b/commands/dns/reverse-record/list.go new file mode 100644 index 000000000..1b831bebb --- /dev/null +++ b/commands/dns/reverse-record/list.go @@ -0,0 +1,59 @@ +package reverse_record + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table/jsonpaths" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + dns "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/cobra" + + "github.com/ionos-cloud/ionosctl/v6/internal/core" +) + +func List() *core.Command { + cmd := core.NewCommand(context.Background(), nil, core.CommandBuilder{ + Namespace: "dns", + Resource: "record", + Verb: "list", + Aliases: []string{"ls", "l"}, + ShortDesc: "Retrieve all reverse records", + Example: "ionosctl dns rr list", + CmdRun: func(c *core.CommandConfig) error { + ls, err := Records(FilterLimitOffset(c.NS), FilterRecordsByIp(c.NS)) + if err != nil { + return fmt.Errorf("failed listing records: %w", err) + } + + items, ok := ls.GetItemsOk() + if !ok || items == nil { + return fmt.Errorf("could not retrieve Record items") + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput("items", jsonpaths.DnsReverseRecord, + ls, tabheaders.GetHeadersAllDefault(allCols, cols)) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + InitClient: true, + }) + + cmd.AddStringFlag(constants.FlagIps, "i", "", "Optional filter for the IP address of the reverse record") + _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return RecordsProperty(func(r dns.ReverseRecordRead) string { + return *r.Properties.Ip + }), cobra.ShellCompDirectiveNoFileComp + }) + cmd.AddInt32Flag(constants.FlagOffset, "", 0, "The first element (of the total list of elements) to include in the response. Use together with limit for pagination") + cmd.AddInt32Flag(constants.FlagMaxResults, "", 0, constants.DescMaxResults) + + return cmd +} diff --git a/commands/dns/reverse-record/reverse-record.go b/commands/dns/reverse-record/reverse-record.go new file mode 100644 index 000000000..efc208882 --- /dev/null +++ b/commands/dns/reverse-record/reverse-record.go @@ -0,0 +1,122 @@ +package reverse_record + +import ( + "context" + "fmt" + + "github.com/gofrs/uuid/v5" + "github.com/ionos-cloud/ionosctl/v6/internal/config" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + "github.com/ionos-cloud/ionosctl/v6/pkg/functional" + "github.com/spf13/viper" + + dns "github.com/ionos-cloud/sdk-go-dns" + + "github.com/ionos-cloud/ionosctl/v6/internal/client" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/spf13/cobra" +) + +var ( + allCols = []string{"Id", "Name", "IP", "Description"} +) + +func Root() *core.Command { + cmd := &core.Command{ + Command: &cobra.Command{ + Use: "reverse-record", + Short: "The sub-commands of 'ionosctl dns reverse-record' allow you to manage DNS reverse records.", + Aliases: []string{"rr"}, + TraverseChildren: true, + }, + } + cmd.Command.PersistentFlags().StringSlice(constants.ArgCols, allCols, tabheaders.ColsMessage(allCols)) + _ = cmd.Command.RegisterFlagCompletionFunc(constants.ArgCols, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return allCols, cobra.ShellCompDirectiveNoFileComp + }) + + cmd.AddCommand(List()) + cmd.AddCommand(Create()) + cmd.AddCommand(Delete()) + cmd.AddCommand(Update()) + cmd.AddCommand(Get()) + return cmd +} + +// RecordsProperty returns a list of properties of all records matching the given filters +func RecordsProperty[V any](f func(dns.ReverseRecordRead) V, fs ...Filter) []V { + recs, err := Records(fs...) + if err != nil { + return nil + } + return functional.Map(*recs.Items, f) +} + +// Records returns all records matching the given filters +func Records(fs ...Filter) (dns.ReverseRecordsReadList, error) { + // Hack to enforce the dns-level flag default for API URL on the completions too + if url := config.GetServerUrl(); url == constants.DefaultApiURL { + viper.Set(constants.ArgServerUrl, "") + } + + req := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsGet(context.Background()) + + for _, f := range fs { + var err error + req, err = f(req) + if err != nil { + return dns.ReverseRecordsReadList{}, err + } + } + + ls, _, err := req.Execute() + if err != nil { + return dns.ReverseRecordsReadList{}, err + } + return ls, nil +} + +// Resolve resolves ipOrId (the IP of a record, or the ID of a record) - to the ID of the record. +// If it's an ID, it's returned as is. If it's not, then it's an IP, and we try to resolve it +func Resolve(ipOrId string) (string, error) { + uid, errParseUuid := uuid.FromString(ipOrId) + rId := uid.String() + if errParseUuid != nil { + // nameOrId is a name + ls, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsGet(context.Background()).FilterRecordIp([]string{ipOrId}).Limit(1).Execute() + if err != nil { + return "", fmt.Errorf("failed finding a record by IP %s: %w", ipOrId, err) + } + if len(*ls.Items) < 1 { + return "", fmt.Errorf("could not find record by IP %s: got %d records", ipOrId, len(*ls.Items)) + } + rId = *(*ls.Items)[0].Id + } + return rId, nil +} + +type Filter func(request dns.ApiReverserecordsGetRequest) (dns.ApiReverserecordsGetRequest, error) + +func FilterRecordsByIp(cmdNs string) Filter { + return func(req dns.ApiReverserecordsGetRequest) (dns.ApiReverserecordsGetRequest, error) { + if fn := core.GetFlagName(cmdNs, constants.FlagIps); viper.IsSet(fn) { + req = req.FilterRecordIp(viper.GetStringSlice(fn)) + } + + return req, nil + } +} + +func FilterLimitOffset(cmdNs string) Filter { + return func(req dns.ApiReverserecordsGetRequest) (dns.ApiReverserecordsGetRequest, error) { + if fn := core.GetFlagName(cmdNs, constants.FlagOffset); viper.IsSet(fn) { + req = req.Offset(viper.GetInt32(fn)) + } + if fn := core.GetFlagName(cmdNs, constants.FlagMaxResults); viper.IsSet(fn) { + req = req.Limit(viper.GetInt32(fn)) + } + + return req, nil + } +} diff --git a/commands/dns/reverse-record/update.go b/commands/dns/reverse-record/update.go new file mode 100644 index 000000000..656144ff4 --- /dev/null +++ b/commands/dns/reverse-record/update.go @@ -0,0 +1,98 @@ +package reverse_record + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/internal/client" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table/jsonpaths" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + "github.com/ionos-cloud/ionosctl/v6/pkg/pointer" + ionoscloud "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func Update() *core.Command { + cmd := core.NewCommand(context.Background(), nil, core.CommandBuilder{ + Namespace: "dns", + Resource: "reverse-record", + Verb: "update", + Aliases: []string{"u", "up"}, + ShortDesc: "Update a record", + Example: "ionosctl dns rr update --record OLD_RECORD_IP --name mail.example.com --ip 5.6.7.8", + PreCmdRun: func(c *core.PreCommandConfig) error { + if err := core.CheckRequiredFlags(c.Command, c.NS, constants.FlagRecord); err != nil { + return err + } + + return nil + }, + CmdRun: func(c *core.CommandConfig) error { + id, err := Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagRecord))) + if err != nil { + return fmt.Errorf("can't resolve IP to a record ID: %s", err) + } + + r, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsFindById(context.Background(), id).Execute() + if err != nil { + return fmt.Errorf("failed querying for reverse record ID %s: %s", id, err) + } + + r.Properties.Name = pointer.From(viper.GetString(core.GetFlagName(c.NS, constants.FlagName))) + r.Properties.Ip = pointer.From(viper.GetString(core.GetFlagName(c.NS, constants.FlagIp))) + r.Properties.Description = pointer.From(viper.GetString(core.GetFlagName(c.NS, constants.FlagDescription))) + + rec, _, err := client.Must().DnsClient.ReverseRecordsApi.ReverserecordsPut(context.Background(), *r.Id). + ReverseRecordEnsure( + ionoscloud.ReverseRecordEnsure{ + Properties: r.Properties, + }). + Execute() + if err != nil { + return fmt.Errorf("failed updating record: %w", err) + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsReverseRecord, rec, + tabheaders.GetHeadersAllDefault(allCols, cols)) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + InitClient: true, + }) + + cmd.AddStringFlag(constants.FlagRecord, "", "", "The record ID or IP, for identifying which record you want to update", core.RequiredFlagOption()) + cmd.Command.RegisterFlagCompletionFunc(constants.FlagRecord, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ips := RecordsProperty(func(read ionoscloud.ReverseRecordRead) string { + return *read.Properties.Ip + }) + return ips, cobra.ShellCompDirectiveNoFileComp + }) + cmd.AddStringFlag(constants.FlagIp, "", "", "The new IP") + cmd.Command.RegisterFlagCompletionFunc(constants.FlagIp, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ipblocks, _, err := client.Must().CloudClient.IPBlocksApi.IpblocksGet(context.Background()).Execute() + if err != nil || ipblocks.Items == nil || len(*ipblocks.Items) == 0 { + return nil, cobra.ShellCompDirectiveError + } + var ips []string + for _, ipblock := range *ipblocks.Items { + if ipblock.Properties.Ips != nil { + ips = append(ips, *ipblock.Properties.Ips...) + } + } + return ips, cobra.ShellCompDirectiveNoFileComp + }) + cmd.AddStringFlag(constants.FlagName, "", "", "The new record name") + cmd.AddStringFlag(constants.FlagDescription, "", "", "The new description of the record") + + cmd.Command.SilenceUsage = true + return cmd +} diff --git a/docs/subcommands/DNS/record/create.md b/docs/subcommands/DNS/record/create.md index bd4019111..1bde26b13 100644 --- a/docs/subcommands/DNS/record/create.md +++ b/docs/subcommands/DNS/record/create.md @@ -1,5 +1,5 @@ --- -description: "Create a record. Wiki: https://docs.ionos.com/dns-as-a-service/readme/api-how-tos/create-a-new-dns-record" +description: "Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/api-how-tos/create-dns-record" --- # DnsRecordCreate @@ -26,7 +26,7 @@ For `create` command: ## Description -Create a record. Wiki: https://docs.ionos.com/dns-as-a-service/readme/api-how-tos/create-a-new-dns-record +Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/api-how-tos/create-dns-record ## Options diff --git a/docs/subcommands/DNS/reverse/record/create.md b/docs/subcommands/DNS/reverse/record/create.md new file mode 100644 index 000000000..3e96bc869 --- /dev/null +++ b/docs/subcommands/DNS/reverse/record/create.md @@ -0,0 +1,54 @@ +--- +description: "Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/api-how-tos/create-and-manage-reverse-dns" +--- + +# DnsReverseRecordCreate + +## Usage + +```text +ionosctl dns reverse-record create [flags] +``` + +## Aliases + +For `reverse-record` command: + +```text +[rr] +``` + +For `create` command: + +```text +[c post] +``` + +## Description + +Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/api-how-tos/create-and-manage-reverse-dns + +## Options + +```text + -u, --api-url string Override default host url (default "dns.de-fra.ionos.com") + --cols strings Set of columns to be printed on output + Available columns: [Id Name IP Description] (default [Id,Name,IP,Description]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + --description string Description stored along with the reverse DNS record to describe its usage + -f, --force Force command to execute without user input + -h, --help Print usage + --ip string [IPv4/IPv6] Specifies for which IP address the reverse record should be created. The IP addresses needs to be owned by the contract (required) + -n, --name string The name of the DNS Reverse Record. (required) + --no-headers Don't print table headers when table output is used + -o, --output string Desired output format [text|json|api-json] (default "text") + -q, --quiet Quiet output + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns rr create --name mail.example.com --ip 5.6.7.8 +``` + diff --git a/docs/subcommands/DNS/reverse/record/delete.md b/docs/subcommands/DNS/reverse/record/delete.md new file mode 100644 index 000000000..e35b6ddf4 --- /dev/null +++ b/docs/subcommands/DNS/reverse/record/delete.md @@ -0,0 +1,55 @@ +--- +description: "Delete a record" +--- + +# DnsReverseRecordDelete + +## Usage + +```text +ionosctl dns reverse-record delete [flags] +``` + +## Aliases + +For `reverse-record` command: + +```text +[rr] +``` + +For `delete` command: + +```text +[d del rm] +``` + +## Description + +Delete a record + +## Options + +```text + -a, --all Delete all records if set (required) + -u, --api-url string Override default host url (default "dns.de-fra.ionos.com") + --cols strings Set of columns to be printed on output + Available columns: [Id Name IP Description] (default [Id,Name,IP,Description]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + -f, --force Force command to execute without user input + -h, --help Print usage + --no-headers Don't print table headers when table output is used + -o, --output string Desired output format [text|json|api-json] (default "text") + -q, --quiet Quiet output + --record string The record ID or IP which you want to delete (required) + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns rr delete -af +ionosctl dns rr delete --record RECORD_IP +ionosctl dns rr delete --record RECORD_ID +``` + diff --git a/docs/subcommands/DNS/reverse/record/get.md b/docs/subcommands/DNS/reverse/record/get.md new file mode 100644 index 000000000..3d6987153 --- /dev/null +++ b/docs/subcommands/DNS/reverse/record/get.md @@ -0,0 +1,53 @@ +--- +description: "Find a record by IP or ID" +--- + +# DnsReverseRecordGet + +## Usage + +```text +ionosctl dns reverse-record get [flags] +``` + +## Aliases + +For `reverse-record` command: + +```text +[rr] +``` + +For `get` command: + +```text +[g] +``` + +## Description + +Find a record by IP or ID + +## Options + +```text + -u, --api-url string Override default host url (default "dns.de-fra.ionos.com") + --cols strings Set of columns to be printed on output + Available columns: [Id Name IP Description] (default [Id,Name,IP,Description]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + -f, --force Force command to execute without user input + -h, --help Print usage + --no-headers Don't print table headers when table output is used + -o, --output string Desired output format [text|json|api-json] (default "text") + -q, --quiet Quiet output + --record string The record ID or IP, for identifying which record you want to update (required) + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns rr get --record RECORD_IP +ionosctl dns rr get --record RECORD_ID +``` + diff --git a/docs/subcommands/DNS/reverse/record/list.md b/docs/subcommands/DNS/reverse/record/list.md new file mode 100644 index 000000000..1d8b23437 --- /dev/null +++ b/docs/subcommands/DNS/reverse/record/list.md @@ -0,0 +1,54 @@ +--- +description: "Retrieve all reverse records" +--- + +# DnsReverseRecordList + +## Usage + +```text +ionosctl dns reverse-record list [flags] +``` + +## Aliases + +For `reverse-record` command: + +```text +[rr] +``` + +For `list` command: + +```text +[ls l] +``` + +## Description + +Retrieve all reverse records + +## Options + +```text + -u, --api-url string Override default host url (default "dns.de-fra.ionos.com") + --cols strings Set of columns to be printed on output + Available columns: [Id Name IP Description] (default [Id,Name,IP,Description]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + -f, --force Force command to execute without user input + -h, --help Print usage + -i, --ips string Optional filter for the IP address of the reverse record + --max-results int32 The maximum number of elements to return + --no-headers Don't print table headers when table output is used + --offset int32 The first element (of the total list of elements) to include in the response. Use together with limit for pagination + -o, --output string Desired output format [text|json|api-json] (default "text") + -q, --quiet Quiet output + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns rr list +``` + diff --git a/docs/subcommands/DNS/reverse/record/update.md b/docs/subcommands/DNS/reverse/record/update.md new file mode 100644 index 000000000..33d275703 --- /dev/null +++ b/docs/subcommands/DNS/reverse/record/update.md @@ -0,0 +1,55 @@ +--- +description: "Update a record" +--- + +# DnsReverseRecordUpdate + +## Usage + +```text +ionosctl dns reverse-record update [flags] +``` + +## Aliases + +For `reverse-record` command: + +```text +[rr] +``` + +For `update` command: + +```text +[u up] +``` + +## Description + +Update a record + +## Options + +```text + -u, --api-url string Override default host url (default "dns.de-fra.ionos.com") + --cols strings Set of columns to be printed on output + Available columns: [Id Name IP Description] (default [Id,Name,IP,Description]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + --description string The new description of the record + -f, --force Force command to execute without user input + -h, --help Print usage + --ip string The new IP + --name string The new record name + --no-headers Don't print table headers when table output is used + -o, --output string Desired output format [text|json|api-json] (default "text") + -q, --quiet Quiet output + --record string The record ID or IP, for identifying which record you want to update (required) + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns rr update --record OLD_RECORD_IP --name mail.example.com --ip 5.6.7.8 +``` + diff --git a/docs/summary.md b/docs/summary.md index e8ae4ac89..1df96d396 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -266,6 +266,13 @@ * [get](subcommands%2FDNS%2Frecord%2Fget.md) * [list](subcommands%2FDNS%2Frecord%2Flist.md) * [update](subcommands%2FDNS%2Frecord%2Fupdate.md) + * reverse + * record + * [create](subcommands%2FDNS%2Freverse%2Frecord%2Fcreate.md) + * [delete](subcommands%2FDNS%2Freverse%2Frecord%2Fdelete.md) + * [get](subcommands%2FDNS%2Freverse%2Frecord%2Fget.md) + * [list](subcommands%2FDNS%2Freverse%2Frecord%2Flist.md) + * [update](subcommands%2FDNS%2Freverse%2Frecord%2Fupdate.md) * zone * [create](subcommands%2FDNS%2Fzone%2Fcreate.md) * [delete](subcommands%2FDNS%2Fzone%2Fdelete.md) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index cdca798d4..e059666f3 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -30,6 +30,8 @@ Within each layer, a token takes precedence over a username and password combina FlagMaxResults = "max-results" FlagMaxResultsShort = "M" FlagCidr = "cidr" + FlagIp = "ip" + FlagIps = "ips" FlagLanId = "lan-id" FlagEdition = "edition" diff --git a/internal/printer/json2table/jsonpaths/dns.go b/internal/printer/json2table/jsonpaths/dns.go index dbfe9aece..6f030f343 100644 --- a/internal/printer/json2table/jsonpaths/dns.go +++ b/internal/printer/json2table/jsonpaths/dns.go @@ -2,12 +2,19 @@ package jsonpaths // DNS json paths var ( + DnsReverseRecord = map[string]string{ + "Id": "id", + "Name": "properties.name", + "Description": "properties.description", + "IP": "properties.ip", + } + DnsRecord = map[string]string{ "Id": "id", "Name": "properties.name", "Content": "properties.content", "Type": "properties.type", - "Enabled": "properties.name", + "Enabled": "properties.enabled", "FQDN": "metadata.fqdn", "State": "metadata.state", "ZoneId": "zoneId",