diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e6b6506..8b475bcec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Added support for `manpages` generation via `ionosctl man` command +- Added support for DNS `secondary-zones` and `zone files` ### Changed - Added authentication warning to `image upload` help text diff --git a/commands/dns/completer/completer.go b/commands/dns/completer/completer.go new file mode 100644 index 000000000..8c800f0f5 --- /dev/null +++ b/commands/dns/completer/completer.go @@ -0,0 +1,71 @@ +package completer + +import ( + "context" + + "github.com/ionos-cloud/ionosctl/v6/internal/client" + "github.com/ionos-cloud/ionosctl/v6/internal/completions" + "github.com/ionos-cloud/ionosctl/v6/internal/config" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table/jsonpaths" + "github.com/ionos-cloud/ionosctl/v6/pkg/functional" + + "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/viper" +) + +func SecondaryZonesIDs() []string { + // 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, "") + } + + secondaryZones, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesGet(context.Background()).Execute() + if err != nil { + return nil + } + + secondaryZonesConverted, err := json2table.ConvertJSONToTable("items", jsonpaths.DnsSecondaryZone, secondaryZones) + if err != nil { + return nil + } + + return completions.NewCompleter( + secondaryZonesConverted, "Id", + ).AddInfo("Name").AddInfo("State", "(%v)").ToString() +} + +// Zones returns all zones matching the given filters +func Zones(fs ...Filter) (ionoscloud.ZoneReadList, 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.ZonesApi.ZonesGet(context.Background()) + + for _, f := range fs { + var err error + req, err = f(req) + if err != nil { + return ionoscloud.ZoneReadList{}, err + } + } + + ls, _, err := req.Execute() + if err != nil { + return ionoscloud.ZoneReadList{}, err + } + return ls, nil +} + +func ZonesProperty[V any](f func(ionoscloud.ZoneRead) V, fs ...Filter) []V { + recs, err := Zones(fs...) + if err != nil { + return nil + } + return functional.Map(*recs.Items, f) +} + +type Filter func(request ionoscloud.ApiZonesGetRequest) (ionoscloud.ApiZonesGetRequest, error) diff --git a/commands/dns/dns.go b/commands/dns/dns.go index 083e61105..39a6af049 100644 --- a/commands/dns/dns.go +++ b/commands/dns/dns.go @@ -5,6 +5,7 @@ import ( "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" + secondary_zones "github.com/ionos-cloud/ionosctl/v6/commands/dns/secondary-zones" "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/spf13/cobra" @@ -23,6 +24,7 @@ func DNSCommand() *core.Command { cmd.AddCommand(reverse_record.Root()) cmd.AddCommand(quota.Root()) cmd.AddCommand(dnssec.Root()) + cmd.AddCommand(secondary_zones.Root()) return cmd } diff --git a/commands/dns/dns_integration_test.go b/commands/dns/dns_integration_test.go index e1a4ae845..aa1a6a67f 100644 --- a/commands/dns/dns_integration_test.go +++ b/commands/dns/dns_integration_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/ionos-cloud/ionosctl/v6/commands/dns/record" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/pkg/functional" dns "github.com/ionos-cloud/sdk-go-dns" @@ -107,9 +108,9 @@ func TestZone(t *testing.T) { assert.NotEmpty(t, sharedZ.Properties) assert.Equal(t, randDesc, *sharedZ.Properties.Description) - resolvedId, err := zone.Resolve(randName) + resolvedId, err := utils.ZoneResolve(randName) assert.NoError(t, err) - assert.Equal(t, *sharedZ.Id, resolvedId) // I added these 3 lines later - to test zone.Resolve too + assert.Equal(t, *sharedZ.Id, resolvedId) // I added these 3 lines later - to test zone.ZoneResolve too // === `ionosctl dns z get` c = zone.ZonesFindByIdCmd() @@ -140,7 +141,7 @@ func TestZone(t *testing.T) { assert.NoError(t, err) assert.Equal(t, randDesc, *zoneThroughSdk.Properties.Description) - resolvedId, err = zone.Resolve(randName) + resolvedId, err = utils.ZoneResolve(randName) assert.NoError(t, err) assert.Equal(t, *sharedZ.Id, resolvedId) } @@ -175,7 +176,7 @@ func TestRecord(t *testing.T) { assert.NotEmpty(t, r.Properties) assert.Equal(t, randIp, *r.Properties.Content) - // also test record.Resolve + // also test record.ZoneResolve resolvedId, err := record.Resolve(randName) assert.NoError(t, err) assert.Equal(t, *r.Id, resolvedId) diff --git a/commands/dns/dnssec/create.go b/commands/dns/dnssec/create.go index c755ab146..283507ae8 100644 --- a/commands/dns/dnssec/create.go +++ b/commands/dns/dnssec/create.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" @@ -43,7 +44,7 @@ func Create() *core.Command { return nil }, CmdRun: func(c *core.CommandConfig) error { - zoneId, err := zone.Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -84,7 +85,7 @@ func Create() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/dnssec/delete.go b/commands/dns/dnssec/delete.go index bf436ae12..21fc41317 100644 --- a/commands/dns/dnssec/delete.go +++ b/commands/dns/dnssec/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" @@ -30,7 +31,7 @@ func Delete() *core.Command { }, CmdRun: func(c *core.CommandConfig) error { zoneName := viper.GetString(core.GetFlagName(c.NS, constants.FlagZone)) - zoneId, err := zone.Resolve(zoneName) + zoneId, err := utils.ZoneResolve(zoneName) if err != nil { return err } @@ -48,7 +49,7 @@ func Delete() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/dnssec/get.go b/commands/dns/dnssec/get.go index d9535a783..32b82b42d 100644 --- a/commands/dns/dnssec/get.go +++ b/commands/dns/dnssec/get.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" @@ -34,7 +35,7 @@ ionosctl dns keys list --zone ZONE --cols PubKey --no-headers`, return nil }, CmdRun: func(c *core.CommandConfig) error { - zoneId, err := zone.Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -63,7 +64,7 @@ ionosctl dns keys list --zone ZONE --cols PubKey --no-headers`, cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/record/create.go b/commands/dns/record/create.go index 04f270aa2..c741f44db 100644 --- a/commands/dns/record/create.go +++ b/commands/dns/record/create.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "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" @@ -11,8 +13,6 @@ import ( "github.com/ionos-cloud/ionosctl/v6/pkg/pointer" "github.com/ionos-cloud/ionosctl/v6/pkg/uuidgen" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" - dns "github.com/ionos-cloud/sdk-go-dns" "github.com/ionos-cloud/ionosctl/v6/internal/client" @@ -41,7 +41,7 @@ func ZonesRecordsPostCmd() *core.Command { input := dns.Record{} modifyRecordPropertiesFromFlags(c, &input) - zoneId, err := zone.Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -73,7 +73,7 @@ func ZonesRecordsPostCmd() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", "The ID or name of the DNS zone", core.RequiredFlagOption()) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/record/delete.go b/commands/dns/record/delete.go index 20761521c..8476a6a92 100644 --- a/commands/dns/record/delete.go +++ b/commands/dns/record/delete.go @@ -6,7 +6,8 @@ import ( "strings" "github.com/gofrs/uuid/v5" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" "github.com/ionos-cloud/ionosctl/v6/pkg/functional" @@ -55,7 +56,7 @@ ionosctl dns r delete --record PARTIAL_NAME --zone ZONE`, return deleteAll(c) } - zoneId, err := zone.Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -97,7 +98,7 @@ ionosctl dns r delete --record PARTIAL_NAME --zone ZONE`, cmd.AddBoolFlag(constants.ArgAll, constants.ArgAllShort, false, fmt.Sprintf("Delete all records. You can optionally filter the deleted records using --%s (full name / ID) and --%s (partial name)", constants.FlagZone, constants.FlagRecord)) cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", fmt.Sprintf("The full name or ID of the zone of the containing the target record. If --%s is set this is applied as a filter - limiting to records within this zone", constants.ArgAll)) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/record/get.go b/commands/dns/record/get.go index 8728ca9e7..068c046b1 100644 --- a/commands/dns/record/get.go +++ b/commands/dns/record/get.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "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" @@ -33,7 +34,7 @@ func ZonesRecordsFindByIdCmd() *core.Command { return nil }, CmdRun: func(c *core.CommandConfig) error { - zoneId, err := zone.Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -51,9 +52,6 @@ func ZonesRecordsFindByIdCmd() *core.Command { } cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) - // if err != nil { - // return err - // } out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsRecord, r, tabheaders.GetHeadersAllDefault(defaultCols, cols)) @@ -69,7 +67,7 @@ func ZonesRecordsFindByIdCmd() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/record/list.go b/commands/dns/record/list.go index cdde5ac03..bfecc27b1 100644 --- a/commands/dns/record/list.go +++ b/commands/dns/record/list.go @@ -3,8 +3,10 @@ package record import ( "context" "fmt" + "slices" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table" @@ -18,83 +20,207 @@ import ( "github.com/spf13/viper" ) +var ( + allColsSecondaryZoneRecord = append(slices.Clone(allCols[:len(allCols)-1]), "RootName") + defaultColsSecondaryZoneRecord = append(slices.Clone(defaultCols[:len(defaultCols)-1]), "RootName") +) + func RecordsGetCmd() *core.Command { - cmd := core.NewCommand(context.Background(), nil, core.CommandBuilder{ - Namespace: "dns", - Resource: "record", - Verb: "list", - Aliases: []string{"ls"}, - ShortDesc: "Retrieve all records", - Example: "ionosctl dns r list", - CmdRun: func(c *core.CommandConfig) error { - ls, err := Records(func(req dns.ApiRecordsGetRequest) (dns.ApiRecordsGetRequest, error) { - if fn := core.GetFlagName(c.NS, constants.FlagZone); viper.IsSet(fn) { - zoneId, err := zone.Resolve(viper.GetString(fn)) - if err != nil { - return req, err - } - req = req.FilterZoneId(zoneId) - } - if fn := core.GetFlagName(c.NS, constants.FlagName); viper.IsSet(fn) { - req = req.FilterName(viper.GetString(fn)) + cmd := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Namespace: "dns", + Resource: "record", + Verb: "list", + Aliases: []string{"ls"}, + ShortDesc: "Retrieve all records from either a primary or secondary zone", + Example: `ionosctl dns r list +ionosctl dns r list --secondary-zone SECONDARY_ZONE_ID +ionosctl dns r list --zone ZONE_ID`, + PreCmdRun: func(c *core.PreCommandConfig) error { + if c.Command.Command.Flags().Changed(constants.FlagZone) && c.Command.Command.Flags().Changed(constants.FlagSecondaryZone) { + return fmt.Errorf("only one of the flags --%s and --%s can be set", constants.FlagZone, constants.FlagSecondaryZone) } - if fn := core.GetFlagName(c.NS, constants.FlagOffset); viper.IsSet(fn) { - req = req.Offset(viper.GetInt32(fn)) + + if c.Command.Command.Flags().Changed(constants.FlagSecondaryZone) && c.Command.Command.Flags().Changed(constants.FlagName) { + return fmt.Errorf("flag --%s is only available for zone records listing", constants.FlagName) } - if fn := core.GetFlagName(c.NS, constants.FlagMaxResults); viper.IsSet(fn) { - req = req.Limit(viper.GetInt32(fn)) + + return core.CheckRequiredFlagsSets(c.Command, c.NS, []string{constants.FlagZone}, []string{constants.FlagSecondaryZone}, []string{}) + }, + CmdRun: func(c *core.CommandConfig) error { + if c.Command.Command.Flags().Changed(constants.FlagSecondaryZone) { + return listSecondaryRecords(c) } - return req, nil - }) - if err != nil { - return fmt.Errorf("failed listing records: %w", err) - } + return listRecordsCmd(c) + }, + InitClient: true, + }, + ) + + cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", "(UUID or Zone Name) Filter used to fetch only the records that contain specified zone.") + _ = cmd.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.ZonesProperty( + func(t dns.ZoneRead) string { + return *t.Properties.ZoneName + }, + ), cobra.ShellCompDirectiveNoFileComp + }, + ) + cmd.AddStringFlag(constants.FlagName, "", "", "Filter used to fetch only the records that contain specified record name. NOTE: Only available for zone records.") + 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) - items, ok := ls.GetItemsOk() - if !ok || items == nil { - return fmt.Errorf("could not retrieve Record items") + cmd.Command.Flags().String(constants.FlagSecondaryZone, "", "The name or ID of the secondary zone to fetch records from") + _ = cmd.Command.RegisterFlagCompletionFunc( + constants.FlagSecondaryZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.SecondaryZonesIDs(), cobra.ShellCompDirectiveNoFileComp + }, + ) + + cmd.Command.PersistentFlags().StringSlice( + constants.ArgCols, nil, + fmt.Sprintf( + "Set of columns to be printed on output \nAvailable columns for primary zones: %v\nAvailable columns for secondary zones: %v", + allCols, allColsSecondaryZoneRecord, + ), + ) + _ = cmd.Command.RegisterFlagCompletionFunc( + constants.ArgCols, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if cmd.Flags().Changed(constants.FlagSecondaryZone) { + return allColsSecondaryZoneRecord, cobra.ShellCompDirectiveNoFileComp } - var lsConverted []map[string]interface{} - for _, item := range *items { - temp, err := json2table.ConvertJSONToTable("", jsonpaths.DnsRecord, item) + return allCols, cobra.ShellCompDirectiveNoFileComp + }, + ) + + return cmd +} + +func listRecordsCmd(c *core.CommandConfig) error { + ls, err := Records( + func(req dns.ApiRecordsGetRequest) (dns.ApiRecordsGetRequest, error) { + if fn := core.GetFlagName(c.NS, constants.FlagZone); viper.IsSet(fn) { + zoneId, err := utils.ZoneResolve(viper.GetString(fn)) if err != nil { - return fmt.Errorf("could not convert from JSON to Table format: %w", err) + return req, err } + req = req.FilterZoneId(zoneId) + } + if fn := core.GetFlagName(c.NS, constants.FlagName); viper.IsSet(fn) { + req = req.FilterName(viper.GetString(fn)) + } + if fn := core.GetFlagName(c.NS, constants.FlagOffset); viper.IsSet(fn) { + req = req.Offset(viper.GetInt32(fn)) + } + if fn := core.GetFlagName(c.NS, constants.FlagMaxResults); viper.IsSet(fn) { + req = req.Limit(viper.GetInt32(fn)) + } + return req, nil + }, + ) + if err != nil { + return fmt.Errorf("failed listing zone records: %w", err) + } - if m, ok := item.GetMetadataOk(); ok && m != nil { - z, _, err := client.Must().DnsClient.ZonesApi.ZonesFindById(context.Background(), *m.ZoneId).Execute() - if err == nil && z.Properties != nil { - temp[0]["ZoneName"] = *z.Properties.ZoneName - } - } + items, ok := ls.GetItemsOk() + if !ok || items == nil { + return fmt.Errorf("could not retrieve Zone Record items") + } + + var lsConverted []map[string]interface{} + for _, item := range *items { + temp, err := json2table.ConvertJSONToTable("", jsonpaths.DnsRecord, item) + if err != nil { + return fmt.Errorf("could not convert from JSON to Table format: %w", err) + } - lsConverted = append(lsConverted, temp[0]) + if m, ok := item.GetMetadataOk(); ok && m != nil { + z, _, err := client.Must().DnsClient.ZonesApi.ZonesFindById(context.Background(), *m.ZoneId).Execute() + if err == nil && z.Properties != nil { + temp[0]["ZoneName"] = *z.Properties.ZoneName } + } - cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + lsConverted = append(lsConverted, temp[0]) + } - out, err := jsontabwriter.GenerateOutputPreconverted(ls, lsConverted, tabheaders.GetHeaders(allCols, defaultCols, cols)) - if err != nil { - return err + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + + out, err := jsontabwriter.GenerateOutputPreconverted(ls, lsConverted, tabheaders.GetHeaders(allCols, defaultCols, cols)) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil +} + +func listSecondaryRecords(c *core.CommandConfig) error { + records, err := secondaryRecords(c) + if err != nil { + return fmt.Errorf("failed listing secondary zone records: %w", err) + } + + items, ok := records.GetItemsOk() + if !ok || items == nil { + return fmt.Errorf("could not retrieve Secondary Zone Record items") + } + + recordsConverted := make([]map[string]interface{}, len(*items)) + for i, item := range *items { + temp, err := json2table.ConvertJSONToTable("", jsonpaths.DnsRecord, item) + if err != nil { + return fmt.Errorf("could not convert from JSON to Table format: %w", err) + } + + if m, ok := item.GetMetadataOk(); ok && m != nil { + z, _, err := client.Must().DnsClient.ZonesApi.ZonesFindById(context.Background(), *m.ZoneId).Execute() + if err == nil && z.Properties != nil { + temp[0]["ZoneName"] = *z.Properties.ZoneName } + } - fmt.Fprintf(c.Command.Command.OutOrStdout(), out) - return nil - }, - InitClient: true, - }) + recordsConverted[i] = temp[0] + } - cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", "(UUID or Zone Name) Filter used to fetch only the records that contain specified zone.") - _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { - return *t.Properties.ZoneName - }), cobra.ShellCompDirectiveNoFileComp - }) - cmd.AddStringFlag(constants.FlagName, "", "", "Filter used to fetch only the records that contain specified record name") - 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) + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutputPreconverted( + records, recordsConverted, tabheaders.GetHeaders(allColsSecondaryZoneRecord, defaultColsSecondaryZoneRecord, cols), + ) + if err != nil { + return err + } - return cmd + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil +} + +func secondaryRecords(c *core.CommandConfig) (dns.SecondaryZoneRecordReadList, error) { + secondaryZoneIDOrName, _ := c.Command.Command.Flags().GetString(constants.FlagSecondaryZone) + secondaryZoneID, err := utils.SecondaryZoneResolve(secondaryZoneIDOrName) + if err != nil { + return dns.SecondaryZoneRecordReadList{}, err + } + + req := client.Must().DnsClient.RecordsApi.SecondaryzonesRecordsGet(context.Background(), secondaryZoneID) + + if c.Command.Command.Flags().Changed(constants.FlagOffset) { + offset, _ := c.Command.Command.Flags().GetInt32(constants.FlagOffset) + req = req.Offset(offset) + } + + if c.Command.Command.Flags().Changed(constants.FlagMaxResults) { + maxResults, _ := c.Command.Command.Flags().GetInt32(constants.FlagMaxResults) + req = req.Limit(maxResults) + } + + records, _, err := req.Execute() + if err != nil { + return dns.SecondaryZoneRecordReadList{}, err + } + + return records, nil } diff --git a/commands/dns/record/record.go b/commands/dns/record/record.go index d41cc54d0..ccb8549fd 100644 --- a/commands/dns/record/record.go +++ b/commands/dns/record/record.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/gofrs/uuid/v5" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "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" @@ -20,7 +20,7 @@ import ( ) var ( - allCols = []string{"Id", "Name", "Content", "Type", "Enabled", "FQDN", "State", "ZoneId", "ZoneName"} + allCols = []string{"Id", "Name", "Content", "Type", "Enabled", "FQDN", "ZoneId", "ZoneName", "State"} defaultCols = []string{"Id", "Name", "Content", "Type", "Enabled", "FQDN", "State"} ) @@ -103,7 +103,7 @@ type Filter func(dns.ApiRecordsGetRequest) (dns.ApiRecordsGetRequest, error) func FilterRecordsByZoneAndRecordFlags(cmdNs string) Filter { return func(req dns.ApiRecordsGetRequest) (dns.ApiRecordsGetRequest, error) { if fn := core.GetFlagName(cmdNs, constants.FlagZone); viper.IsSet(fn) { - zoneId, err := zone.Resolve(viper.GetString(fn)) + zoneId, err := utils.ZoneResolve(viper.GetString(fn)) if err != nil { return req, err } diff --git a/commands/dns/record/update.go b/commands/dns/record/update.go index fa24c2980..025eae911 100644 --- a/commands/dns/record/update.go +++ b/commands/dns/record/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/client" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/printer/json2table/jsonpaths" @@ -33,7 +34,7 @@ func ZonesRecordsPutCmd() *core.Command { return nil }, CmdRun: func(c *core.CommandConfig) error { - zoneId, err := zone.Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -52,7 +53,7 @@ func ZonesRecordsPutCmd() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone, core.RequiredFlagOption()) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return zone.ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/secondary-zones/create.go b/commands/dns/secondary-zones/create.go new file mode 100644 index 000000000..ce43b6976 --- /dev/null +++ b/commands/dns/secondary-zones/create.go @@ -0,0 +1,83 @@ +package secondary_zones + +import ( + "context" + "fmt" + "net" + + "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" + dns "github.com/ionos-cloud/sdk-go-dns" +) + +func createCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "create", + Aliases: []string{"c"}, + ShortDesc: "Create a secondary zone", + LongDesc: `Create a new secondary zone with default NS and SOA records. Note that Cloud DNS relies on the following Anycast addresses for sending DNS notify messages. Make sure to whitelist on your end: + +IPv4: 212.227.123.25 +IPv6: 2001:8d8:fe:53::5cd:25`, + Example: "ionosctl dns secondary-zone create --name ZONE_NAME --description DESCRIPTION --primary-ips 1.2.3.4,5.6.7.8", + PreCmdRun: func(c *core.PreCommandConfig) error { + if err := core.CheckRequiredFlags(c.Command, c.NS, constants.FlagName, constants.FlagPrimaryIPs); err != nil { + return err + } + + // Validate primary IPs + primaryIPs, _ := c.Command.Command.Flags().GetStringSlice(constants.FlagPrimaryIPs) + for _, ip := range primaryIPs { + if net.ParseIP(ip) == nil { + return fmt.Errorf("invalid IP address: %s", ip) + } + } + + return nil + }, + CmdRun: func(c *core.CommandConfig) error { + name, _ := c.Command.Command.Flags().GetString(constants.FlagName) + description, _ := c.Command.Command.Flags().GetString(constants.FlagDescription) + primaryIPs, _ := c.Command.Command.Flags().GetStringSlice(constants.FlagPrimaryIPs) + + secZoneProps := dns.SecondaryZone{ + ZoneName: &name, + Description: &description, + PrimaryIps: &primaryIPs, + } + + secZone, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesPost(context.Background()).SecondaryZoneCreate( + *dns.NewSecondaryZoneCreate(secZoneProps), + ).Execute() + if err != nil { + return err + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput( + "", jsonpaths.DnsSecondaryZone, secZone, tabheaders.GetHeadersAllDefault(allCols, cols), + ) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + }, + ) + + c.Command.Flags().StringP(constants.FlagName, constants.FlagNameShort, "", "Name of the secondary zone") + c.Command.Flags().String(constants.FlagDescription, "", "Description of the secondary zone") + c.Command.Flags().StringSlice(constants.FlagPrimaryIPs, []string{}, "Primary DNS server IP addresses") + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/secondary-zones/delete.go b/commands/dns/secondary-zones/delete.go new file mode 100644 index 000000000..eeac68d5a --- /dev/null +++ b/commands/dns/secondary-zones/delete.go @@ -0,0 +1,97 @@ +package secondary_zones + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" + "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/jsontabwriter" + "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" + "github.com/ionos-cloud/ionosctl/v6/pkg/functional" + "github.com/spf13/cobra" + + dns "github.com/ionos-cloud/sdk-go-dns" + "github.com/spf13/viper" +) + +func deleteCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "delete", + Aliases: []string{"d", "del"}, + ShortDesc: "Delete a secondary zone", + LongDesc: "Delete a secondary zone", + Example: "ionosctl dns secondary-zone delete --zone ZONE_ID", + PreCmdRun: func(c *core.PreCommandConfig) error { + return core.CheckRequiredFlagsSets(c.Command, c.NS, []string{constants.ArgAll}, []string{constants.FlagZone}) + }, + CmdRun: func(c *core.CommandConfig) error { + if all, _ := c.Command.Command.Flags().GetBool(constants.ArgAll); c.Command.Command.Flags().Changed(constants.ArgAll) && all { + return deleteAll(c) + } + + zone, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + zoneID, err := utils.SecondaryZoneResolve(zone) + if err != nil { + return err + } + + if err = deleteSingle(c, zoneID); err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), jsontabwriter.GenerateLogOutput("Successfully deleted secondary zone %v", zoneID)) + return nil + }, + }, + ) + + c.Command.Flags().BoolP(constants.ArgAll, constants.ArgAllShort, false, "Delete all secondary zones") + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.SecondaryZonesIDs(), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} + +func deleteAll(c *core.CommandConfig) error { + secZones, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesGet(context.Background()).Execute() + if err != nil { + return err + } + + if err = functional.ApplyAndAggregateErrors( + *secZones.Items, func(item dns.SecondaryZoneRead) error { + return deleteSingle(c, *item.Id) + }, + ); err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), jsontabwriter.GenerateLogOutput("Successfully deleted all secondary zones")) + return nil +} + +func deleteSingle(c *core.CommandConfig, zoneId string) error { + if !confirm.FAsk(c.Command.Command.InOrStdin(), fmt.Sprintf("delete secondary zone %s", zoneId), viper.GetBool(constants.ArgForce)) { + return fmt.Errorf(confirm.UserDenied) + } + + _, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesDelete(context.Background(), zoneId).Execute() + if err != nil { + return err + } + + return nil +} diff --git a/commands/dns/secondary-zones/get.go b/commands/dns/secondary-zones/get.go new file mode 100644 index 000000000..4749fda0d --- /dev/null +++ b/commands/dns/secondary-zones/get.go @@ -0,0 +1,65 @@ +package secondary_zones + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" + "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/spf13/cobra" +) + +func getCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "get", + ShortDesc: "Retrieve a secondary zone", + LongDesc: "Retrieve a secondary zone by its ID or name", + Example: "ionosctl dns secondary-zones get --zone ZONE_ID", + PreCmdRun: func(c *core.PreCommandConfig) error { + return core.CheckRequiredFlags(c.Command, c.NS, constants.FlagZone) + }, + CmdRun: func(c *core.CommandConfig) error { + zoneNameOrID, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + zoneID, err := utils.SecondaryZoneResolve(zoneNameOrID) + if err != nil { + return err + } + + secZone, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesFindById(context.Background(), zoneID).Execute() + if err != nil { + return err + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput( + "", jsonpaths.DnsSecondaryZone, secZone, tabheaders.GetHeadersAllDefault(allCols, cols), + ) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + }, + ) + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.SecondaryZonesIDs(), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/secondary-zones/list.go b/commands/dns/secondary-zones/list.go new file mode 100644 index 000000000..a07f9c2b5 --- /dev/null +++ b/commands/dns/secondary-zones/list.go @@ -0,0 +1,85 @@ +package secondary_zones + +import ( + "context" + "fmt" + "strings" + + "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/spf13/cobra" + + dns "github.com/ionos-cloud/sdk-go-dns" +) + +func listCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "list", + ShortDesc: "List secondary zones", + LongDesc: "List all secondary zones. Default limit is the first 100 items. Use pagination query parameters for listing more items (up to 1000).", + Example: "ionosctl dns secondary-zone list", + PreCmdRun: nil, + CmdRun: func(c *core.CommandConfig) error { + req := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesGet(context.Background()) + + if c.Command.Command.Flags().Changed(constants.FlagName) { + name, _ := c.Command.Command.Flags().GetString(constants.FlagName) + req = req.FilterZoneName(name) + } + + if c.Command.Command.Flags().Changed(constants.FlagState) { + state, _ := c.Command.Command.Flags().GetString(constants.FlagState) + req = req.FilterState(dns.ProvisioningState(state)) + } + + if c.Command.Command.Flags().Changed(constants.FlagMaxResults) { + maxResults, _ := c.Command.Command.Flags().GetInt32(constants.FlagMaxResults) + req = req.Limit(maxResults) + } + + if c.Command.Command.Flags().Changed(constants.FlagOffset) { + offset, _ := c.Command.Command.Flags().GetInt32(constants.FlagOffset) + req = req.Offset(offset) + } + + secZones, _, err := req.Execute() + if err != nil { + return err + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput( + "items", jsonpaths.DnsSecondaryZone, secZones, tabheaders.GetHeadersAllDefault(allCols, cols), + ) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + }, + ) + + enumStates := []string{"AVAILABLE", "FAILED", "PROVISIONING", "DESTROYING"} + c.AddStringFlag(constants.FlagState, "", "", fmt.Sprintf("Filter used to fetch all zones in a particular state (%s)", strings.Join(enumStates, ", "))) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagState, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return enumStates, cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.Flags().StringP(constants.FlagName, constants.FlagNameShort, "", "Filter used to fetch only the zones that contain the specified zone name") + c.Command.Flags().Int32(constants.FlagMaxResults, 0, "Pagination limit") + c.Command.Flags().Int32(constants.FlagOffset, 0, "Pagination offset") + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/secondary-zones/secondary-zones.go b/commands/dns/secondary-zones/secondary-zones.go new file mode 100644 index 000000000..95276cc96 --- /dev/null +++ b/commands/dns/secondary-zones/secondary-zones.go @@ -0,0 +1,38 @@ +package secondary_zones + +import ( + "github.com/ionos-cloud/ionosctl/v6/commands/dns/secondary-zones/transfer" + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + "github.com/spf13/cobra" +) + +var allCols = []string{"Id", "Name", "Description", "PrimaryIPs", "State"} + +func Root() *core.Command { + cmd := &core.Command{ + Command: &cobra.Command{ + Use: "secondary-zone", + Aliases: []string{"secondary-zones", "sz"}, + Short: "All commands related to secondary zones", + 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(transfer.Root()) + cmd.AddCommand(createCmd()) + cmd.AddCommand(deleteCmd()) + cmd.AddCommand(listCmd()) + cmd.AddCommand(getCmd()) + cmd.AddCommand(updateCmd()) + + return cmd +} diff --git a/commands/dns/secondary-zones/transfer/get.go b/commands/dns/secondary-zones/transfer/get.go new file mode 100644 index 000000000..54894bff0 --- /dev/null +++ b/commands/dns/secondary-zones/transfer/get.go @@ -0,0 +1,65 @@ +package transfer + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" + "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/spf13/cobra" +) + +func getCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "get", + Aliases: []string{"g"}, + ShortDesc: "Get the transfer status for a secondary zone", + LongDesc: "Get the transfer status for a secondary zone", + PreCmdRun: func(c *core.PreCommandConfig) error { + return core.CheckRequiredFlags(c.Command, c.NS, constants.FlagZone) + }, + CmdRun: func(c *core.CommandConfig) error { + zoneNameOrID, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + zoneID, err := utils.SecondaryZoneResolve(zoneNameOrID) + if err != nil { + return err + } + + transferStatuses, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesAxfrGet(context.Background(), zoneID).Execute() + if err != nil { + return err + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput( + "items", jsonpaths.DnsSecondaryZoneTransfer, transferStatuses, tabheaders.GetHeadersAllDefault(allCols, cols), + ) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + }, + ) + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.SecondaryZonesIDs(), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/secondary-zones/transfer/start.go b/commands/dns/secondary-zones/transfer/start.go new file mode 100644 index 000000000..bfb3936a2 --- /dev/null +++ b/commands/dns/secondary-zones/transfer/start.go @@ -0,0 +1,55 @@ +package transfer + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" + "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/jsontabwriter" + "github.com/spf13/cobra" +) + +func startCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "start", + Aliases: []string{"s"}, + ShortDesc: "Initiate zone transfer", + LongDesc: "Initiate zone transfer", + PreCmdRun: func(c *core.PreCommandConfig) error { + return core.CheckRequiredFlags(c.Command, c.NS, constants.FlagZone) + }, + CmdRun: func(c *core.CommandConfig) error { + zoneNameOrID, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + zoneID, err := utils.SecondaryZoneResolve(zoneNameOrID) + if err != nil { + return err + } + + _, _, err = client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesAxfrPut(context.Background(), zoneID).Execute() + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), jsontabwriter.GenerateLogOutput("Transfer started for zone %s", zoneID)) + return nil + }, + }, + ) + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.SecondaryZonesIDs(), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/secondary-zones/transfer/transfer.go b/commands/dns/secondary-zones/transfer/transfer.go new file mode 100644 index 000000000..c20a24dc5 --- /dev/null +++ b/commands/dns/secondary-zones/transfer/transfer.go @@ -0,0 +1,33 @@ +package transfer + +import ( + "github.com/ionos-cloud/ionosctl/v6/internal/constants" + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" + "github.com/spf13/cobra" +) + +var allCols = []string{"PrimaryIP", "Status", "ErrorMessage"} + +func Root() *core.Command { + cmd := &core.Command{ + Command: &cobra.Command{ + Use: "transfer", + Aliases: []string{"t"}, + Short: "Manage secondary zone transfers", + 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(getCmd()) + cmd.AddCommand(startCmd()) + + return cmd +} diff --git a/commands/dns/secondary-zones/update.go b/commands/dns/secondary-zones/update.go new file mode 100644 index 000000000..0ddcce5e1 --- /dev/null +++ b/commands/dns/secondary-zones/update.go @@ -0,0 +1,117 @@ +package secondary_zones + +import ( + "context" + "fmt" + "net" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" + "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/spf13/cobra" + + dns "github.com/ionos-cloud/sdk-go-dns" +) + +func updateCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "update", + ShortDesc: "Update or create a secondary zone", + LongDesc: "Update or create a secondary zone", + Example: "ionosctl dns secondary-zone update --zone ZONE_ID --name ZONE_NAME --description DESCRIPTION --primary-ips 1.2.3.4,5.6.7.8", + PreCmdRun: func(c *core.PreCommandConfig) error { + if err := core.CheckRequiredFlags(c.Command, c.NS, constants.FlagZone); err != nil { + return err + } + + if !c.Command.Command.Flags().Changed(constants.FlagDescription) && + !c.Command.Command.Flags().Changed(constants.FlagPrimaryIPs) { + return fmt.Errorf( + "at least one of the following flags must be set: %s, %s", + constants.FlagDescription, constants.FlagPrimaryIPs, + ) + } + + // Validate primary IPs + primaryIPs, _ := c.Command.Command.Flags().GetStringSlice(constants.FlagPrimaryIPs) + for _, ip := range primaryIPs { + if net.ParseIP(ip) == nil { + return fmt.Errorf("invalid IP address: %s", ip) + } + } + + return nil + }, + CmdRun: func(c *core.CommandConfig) error { + zoneNameOrID, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + zoneID, err := utils.SecondaryZoneResolve(zoneNameOrID) + if err != nil { + return err + } + + secZoneProps, err := setSecondaryZoneProperties(c, zoneID) + if err != nil { + return err + } + + secZone, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesPut( + context.Background(), zoneID, + ).SecondaryZoneEnsure(*dns.NewSecondaryZoneEnsure(secZoneProps)).Execute() + if err != nil { + return err + } + + cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) + out, err := jsontabwriter.GenerateOutput( + "", jsonpaths.DnsSecondaryZone, secZone, tabheaders.GetHeadersAllDefault(allCols, cols), + ) + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), out) + return nil + }, + }, + ) + + c.Command.Flags().String(constants.FlagDescription, "", "Description of the secondary zone") + c.Command.Flags().StringSlice(constants.FlagPrimaryIPs, []string{}, "Primary DNS server IP addresses") + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.SecondaryZonesIDs(), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} + +func setSecondaryZoneProperties(c *core.CommandConfig, zoneID string) (dns.SecondaryZone, error) { + currentZone, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesFindById(context.Background(), zoneID).Execute() + if err != nil { + return dns.SecondaryZone{}, err + } + + if c.Command.Command.Flags().Changed(constants.FlagDescription) { + description, _ := c.Command.Command.Flags().GetString(constants.FlagDescription) + currentZone.Properties.Description = &description + } + + if c.Command.Command.Flags().Changed(constants.FlagPrimaryIPs) { + primaryIPs, _ := c.Command.Command.Flags().GetStringSlice(constants.FlagPrimaryIPs) + currentZone.Properties.PrimaryIps = &primaryIPs + } + + return *currentZone.Properties, nil +} diff --git a/commands/dns/utils/utils.go b/commands/dns/utils/utils.go new file mode 100644 index 000000000..5f27e10a6 --- /dev/null +++ b/commands/dns/utils/utils.go @@ -0,0 +1,46 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/gofrs/uuid/v5" + "github.com/ionos-cloud/ionosctl/v6/internal/client" +) + +// SecondaryZoneResolve resolves nameOrId (the name of a zone, or the ID of a zone) - to the ID of the secondary zone. +// If it's an ID, it's returned as is. If it's not, then it's a name, and we try to resolve it +func SecondaryZoneResolve(nameOrID string) (string, error) { + if _, err := uuid.FromString(nameOrID); err == nil { + return nameOrID, nil + } + + secZones, _, err := client.Must().DnsClient.SecondaryZonesApi.SecondaryzonesGet(context.Background()).FilterZoneName(nameOrID).Limit(1).Execute() + if err != nil { + return "", fmt.Errorf("failed to retrieve zones by name %s: %w", nameOrID, err) + } + if secZones.Items == nil || len(*secZones.Items) < 1 { + return "", fmt.Errorf("no zones found with name %s", nameOrID) + } + + return *(*secZones.Items)[0].Id, nil +} + +// ZoneResolve resolves nameOrId (the name of a zone, or the ID of a zone) - to the ID of the zone. +// If it's an ID, it's returned as is. If it's not, then it's a name, and we try to resolve it +func ZoneResolve(nameOrId string) (string, error) { + uid, errParseUuid := uuid.FromString(nameOrId) + zId := uid.String() + if errParseUuid != nil { + // nameOrId is a name + ls, _, errFindZoneByName := client.Must().DnsClient.ZonesApi.ZonesGet(context.Background()).FilterZoneName(nameOrId).Limit(1).Execute() + if errFindZoneByName != nil { + return "", fmt.Errorf("failed finding a zone by name: %w", errFindZoneByName) + } + if len(*ls.Items) < 1 { + return "", fmt.Errorf("could not find zone by name %s: got %d zones", nameOrId, len(*ls.Items)) + } + zId = *(*ls.Items)[0].Id + } + return zId, nil +} diff --git a/commands/dns/zone/create.go b/commands/dns/zone/create.go index 8d82e4a7b..28af516e2 100644 --- a/commands/dns/zone/create.go +++ b/commands/dns/zone/create.go @@ -55,9 +55,6 @@ func ZonesPostCmd() *core.Command { } cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) - // if err != nil { - // return err - // } out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsZone, z, tabheaders.GetHeadersAllDefault(allCols, cols)) if err != nil { diff --git a/commands/dns/zone/delete.go b/commands/dns/zone/delete.go index fb21ec614..2c217c5f1 100644 --- a/commands/dns/zone/delete.go +++ b/commands/dns/zone/delete.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter" "github.com/ionos-cloud/ionosctl/v6/pkg/confirm" @@ -33,7 +35,7 @@ func ZonesDeleteCmd() *core.Command { return deleteAll(c) } - zoneId, err := Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -57,7 +59,7 @@ func ZonesDeleteCmd() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", fmt.Sprintf("%s. Required or -%s", constants.DescZone, constants.ArgAllShort)) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/zone/file/file.go b/commands/dns/zone/file/file.go new file mode 100644 index 000000000..80a4695f9 --- /dev/null +++ b/commands/dns/zone/file/file.go @@ -0,0 +1,22 @@ +package file + +import ( + "github.com/ionos-cloud/ionosctl/v6/internal/core" + "github.com/spf13/cobra" +) + +func Root() *core.Command { + cmd := &core.Command{ + Command: &cobra.Command{ + Use: "file", + Aliases: []string{"f"}, + Short: "All commands related to zone files", + TraverseChildren: true, + }, + } + + cmd.AddCommand(getCmd()) + cmd.AddCommand(updateCmd()) + + return cmd +} diff --git a/commands/dns/zone/file/get.go b/commands/dns/zone/file/get.go new file mode 100644 index 000000000..84f4aeafa --- /dev/null +++ b/commands/dns/zone/file/get.go @@ -0,0 +1,56 @@ +package file + +import ( + "context" + "fmt" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "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/jsontabwriter" + "github.com/spf13/cobra" + + dns "github.com/ionos-cloud/sdk-go-dns" +) + +func getCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "get", + Aliases: []string{"g"}, + ShortDesc: "Get a specific zone file", + LongDesc: "Get the exported zone file in BIND format (RFC 1035)", + PreCmdRun: func(c *core.PreCommandConfig) error { + return core.CheckRequiredFlags(c.Command, c.NS, constants.FlagZone) + }, + CmdRun: func(c *core.CommandConfig) error { + zoneID, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + + resp, err := client.Must().DnsClient.ZoneFilesApi.ZonesZonefileGet(context.Background(), zoneID).Execute() + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), jsontabwriter.GenerateLogOutput(string(resp.Payload))) + return nil + }, + }, + ) + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.ZonesProperty( + func(t dns.ZoneRead) string { + return *t.Properties.ZoneName + }, + ), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/zone/file/update.go b/commands/dns/zone/file/update.go new file mode 100644 index 000000000..791b93dd6 --- /dev/null +++ b/commands/dns/zone/file/update.go @@ -0,0 +1,70 @@ +package file + +import ( + "context" + "fmt" + "os" + + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" + "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/jsontabwriter" + "github.com/spf13/cobra" + + dns "github.com/ionos-cloud/sdk-go-dns" +) + +func updateCmd() *core.Command { + c := core.NewCommand( + context.Background(), nil, core.CommandBuilder{ + Verb: "update", + ShortDesc: "Update a zone file", + LongDesc: "Update a zone file", + Example: "ionosctl dns zone file update --zone ZONE_ID --zone-file FILE_PATH", + PreCmdRun: func(c *core.PreCommandConfig) error { + return core.CheckRequiredFlags(c.Command, c.NS, constants.FlagZone, constants.FlagZoneFile) + }, + CmdRun: func(c *core.CommandConfig) error { + zoneNameOrID, _ := c.Command.Command.Flags().GetString(constants.FlagZone) + zoneID, err := utils.ZoneResolve(zoneNameOrID) + if err != nil { + return err + } + + zoneFilePath, _ := c.Command.Command.Flags().GetString(constants.FlagZoneFile) + body, err := os.ReadFile(zoneFilePath) + if err != nil { + return fmt.Errorf("failed to read zone file: %s", err) + } + + _, _, err = client.Must().DnsClient.ZoneFilesApi.ZonesZonefilePut(context.Background(), zoneID).Body(string(body)).Execute() + if err != nil { + return err + } + + fmt.Fprintf(c.Command.Command.OutOrStdout(), jsontabwriter.GenerateLogOutput("Zone file updated for zone %s", zoneID)) + return nil + }, + }, + ) + + c.Command.Flags().StringP(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone) + _ = c.Command.RegisterFlagCompletionFunc( + constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completer.ZonesProperty( + func(t dns.ZoneRead) string { + return *t.Properties.ZoneName + }, + ), cobra.ShellCompDirectiveNoFileComp + }, + ) + + c.Command.Flags().String(constants.FlagZoneFile, "", "Path to the zone file") + + c.Command.SilenceUsage = true + c.Command.Flags().SortFlags = false + + return c +} diff --git a/commands/dns/zone/get.go b/commands/dns/zone/get.go index c622da9d1..0cc5a9167 100644 --- a/commands/dns/zone/get.go +++ b/commands/dns/zone/get.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "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" @@ -33,7 +35,7 @@ func ZonesFindByIdCmd() *core.Command { return nil }, CmdRun: func(c *core.CommandConfig) error { - zoneId, err := Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + zoneId, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -45,9 +47,6 @@ func ZonesFindByIdCmd() *core.Command { } cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) - // if err != nil { - // return err - // } out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsZone, z, tabheaders.GetHeadersAllDefault(allCols, cols)) if err != nil { @@ -62,7 +61,7 @@ func ZonesFindByIdCmd() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone, core.RequiredFlagOption()) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/zone/list.go b/commands/dns/zone/list.go index c3360fa0a..590e4bc51 100644 --- a/commands/dns/zone/list.go +++ b/commands/dns/zone/list.go @@ -51,9 +51,6 @@ func ZonesGetCmd() *core.Command { } cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) - // if err != nil { - // return err - // } out, err := jsontabwriter.GenerateOutput("items", jsonpaths.DnsZone, ls, tabheaders.GetHeadersAllDefault(allCols, cols)) diff --git a/commands/dns/zone/update.go b/commands/dns/zone/update.go index a820dc9e3..bacd972d3 100644 --- a/commands/dns/zone/update.go +++ b/commands/dns/zone/update.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/completer" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/utils" "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" @@ -34,7 +36,7 @@ func ZonesPutCmd() *core.Command { return nil }, CmdRun: func(c *core.CommandConfig) error { - id, err := Resolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) + id, err := utils.ZoneResolve(viper.GetString(core.GetFlagName(c.NS, constants.FlagZone))) if err != nil { return err } @@ -61,9 +63,6 @@ func ZonesPutCmd() *core.Command { } cols, _ := c.Command.Command.Flags().GetStringSlice(constants.ArgCols) - // if err != nil { - // return err - // } out, err := jsontabwriter.GenerateOutput("", jsonpaths.DnsZone, zNew, tabheaders.GetHeadersAllDefault(allCols, cols)) if err != nil { @@ -78,7 +77,7 @@ func ZonesPutCmd() *core.Command { cmd.AddStringFlag(constants.FlagZone, constants.FlagZoneShort, "", constants.DescZone, core.RequiredFlagOption()) _ = cmd.Command.RegisterFlagCompletionFunc(constants.FlagZone, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return ZonesProperty(func(t dns.ZoneRead) string { + return completer.ZonesProperty(func(t dns.ZoneRead) string { return *t.Properties.ZoneName }), cobra.ShellCompDirectiveNoFileComp }) diff --git a/commands/dns/zone/zone.go b/commands/dns/zone/zone.go index ec40457a3..fea19d552 100644 --- a/commands/dns/zone/zone.go +++ b/commands/dns/zone/zone.go @@ -1,19 +1,11 @@ package zone import ( - "context" - "fmt" - - "github.com/gofrs/uuid/v5" - "github.com/ionos-cloud/ionosctl/v6/internal/client" - "github.com/ionos-cloud/ionosctl/v6/internal/config" + "github.com/ionos-cloud/ionosctl/v6/commands/dns/zone/file" "github.com/ionos-cloud/ionosctl/v6/internal/constants" "github.com/ionos-cloud/ionosctl/v6/internal/core" "github.com/ionos-cloud/ionosctl/v6/internal/printer/tabheaders" - "github.com/ionos-cloud/ionosctl/v6/pkg/functional" - dns "github.com/ionos-cloud/sdk-go-dns" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var ( @@ -31,68 +23,18 @@ func ZoneCommand() *core.Command { } cmd.Command.PersistentFlags().StringSlice(constants.ArgCols, nil, tabheaders.ColsMessage(allCols)) - _ = cmd.Command.RegisterFlagCompletionFunc(constants.ArgCols, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return allCols, cobra.ShellCompDirectiveNoFileComp - }) + _ = cmd.Command.RegisterFlagCompletionFunc( + constants.ArgCols, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return allCols, cobra.ShellCompDirectiveNoFileComp + }, + ) cmd.AddCommand(ZonesGetCmd()) cmd.AddCommand(ZonesDeleteCmd()) cmd.AddCommand(ZonesPostCmd()) cmd.AddCommand(ZonesPutCmd()) cmd.AddCommand(ZonesFindByIdCmd()) + cmd.AddCommand(file.Root()) return cmd } - -// Zones returns all zones matching the given filters -func Zones(fs ...Filter) (dns.ZoneReadList, 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.ZonesApi.ZonesGet(context.Background()) - - for _, f := range fs { - var err error - req, err = f(req) - if err != nil { - return dns.ZoneReadList{}, err - } - } - - ls, _, err := req.Execute() - if err != nil { - return dns.ZoneReadList{}, err - } - return ls, nil -} - -func ZonesProperty[V any](f func(dns.ZoneRead) V, fs ...Filter) []V { - recs, err := Zones(fs...) - if err != nil { - return nil - } - return functional.Map(*recs.Items, f) -} - -type Filter func(request dns.ApiZonesGetRequest) (dns.ApiZonesGetRequest, error) - -// Resolve resolves nameOrId (the name of a zone, or the ID of a zone) - to the ID of the zone. -// If it's an ID, it's returned as is. If it's not, then it's a name, and we try to resolve it -func Resolve(nameOrId string) (string, error) { - uid, errParseUuid := uuid.FromString(nameOrId) - zId := uid.String() - if errParseUuid != nil { - // nameOrId is a name - ls, _, errFindZoneByName := client.Must().DnsClient.ZonesApi.ZonesGet(context.Background()).FilterZoneName(nameOrId).Limit(1).Execute() - if errFindZoneByName != nil { - return "", fmt.Errorf("failed finding a zone by name: %w", errFindZoneByName) - } - if len(*ls.Items) < 1 { - return "", fmt.Errorf("could not find zone by name %s: got %d zones", nameOrId, len(*ls.Items)) - } - zId = *(*ls.Items)[0].Id - } - return zId, nil -} diff --git a/docs/subcommands/DNS/record/create.md b/docs/subcommands/DNS/record/create.md index 1bde26b13..8306e6f2c 100644 --- a/docs/subcommands/DNS/record/create.md +++ b/docs/subcommands/DNS/record/create.md @@ -33,7 +33,7 @@ Create a record. Wiki: https://docs.ionos.com/cloud/network-services/cloud-dns/a ```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 Content Type Enabled FQDN State ZoneId ZoneName] + Available columns: [Id Name Content Type Enabled FQDN ZoneId ZoneName State] -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") --content string The content (Record Data) for your chosen record type. For example, if --type A, --content should be an IPv4 IP. (required) --enabled When true - the record is visible for lookup (default true) diff --git a/docs/subcommands/DNS/record/delete.md b/docs/subcommands/DNS/record/delete.md index ea4a58c4b..e344e5930 100644 --- a/docs/subcommands/DNS/record/delete.md +++ b/docs/subcommands/DNS/record/delete.md @@ -44,7 +44,7 @@ Here, PARTIAL_NAME is a part of the name of the DNS record you want to delete. I -a, --all Delete all records. You can optionally filter the deleted records using --zone (full name / ID) and --record (partial name) -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 Content Type Enabled FQDN State ZoneId ZoneName] + Available columns: [Id Name Content Type Enabled FQDN ZoneId ZoneName State] -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 diff --git a/docs/subcommands/DNS/record/get.md b/docs/subcommands/DNS/record/get.md index 2581d215b..5e2bc5463 100644 --- a/docs/subcommands/DNS/record/get.md +++ b/docs/subcommands/DNS/record/get.md @@ -33,7 +33,7 @@ Retrieve a record ```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 Content Type Enabled FQDN State ZoneId ZoneName] + Available columns: [Id Name Content Type Enabled FQDN ZoneId ZoneName State] -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 diff --git a/docs/subcommands/DNS/record/list.md b/docs/subcommands/DNS/record/list.md index 746ccc38e..8d24a8168 100644 --- a/docs/subcommands/DNS/record/list.md +++ b/docs/subcommands/DNS/record/list.md @@ -1,5 +1,5 @@ --- -description: "Retrieve all records" +description: "Retrieve all records from either a primary or secondary zone" --- # DnsRecordList @@ -26,30 +26,34 @@ For `list` command: ## Description -Retrieve all records +Retrieve all records from either a primary or secondary zone ## 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 Content Type Enabled FQDN State ZoneId ZoneName] - -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 - --max-results int32 The maximum number of elements to return - --name string Filter used to fetch only the records that contain specified record name - --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 - -z, --zone string (UUID or Zone Name) Filter used to fetch only the records that contain specified zone. + -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 for primary zones: [Id Name Content Type Enabled FQDN ZoneId ZoneName State] + Available columns for secondary zones: [Id Name Content Type Enabled FQDN ZoneId ZoneName RootName] + -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 + --max-results int32 The maximum number of elements to return + --name string Filter used to fetch only the records that contain specified record name. NOTE: Only available for zone records. + --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 + --secondary-zone string The name or ID of the secondary zone to fetch records from + -v, --verbose Print step-by-step process when running command + -z, --zone string (UUID or Zone Name) Filter used to fetch only the records that contain specified zone. ``` ## Examples ```text ionosctl dns r list +ionosctl dns r list --secondary-zone SECONDARY_ZONE_ID +ionosctl dns r list --zone ZONE_ID ``` diff --git a/docs/subcommands/DNS/record/update.md b/docs/subcommands/DNS/record/update.md index 86c40261a..c1e7cd57d 100644 --- a/docs/subcommands/DNS/record/update.md +++ b/docs/subcommands/DNS/record/update.md @@ -33,7 +33,7 @@ Partially modify a record's properties. This command uses a combination of GET a ```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 Content Type Enabled FQDN State ZoneId ZoneName] + Available columns: [Id Name Content Type Enabled FQDN ZoneId ZoneName State] -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") --content string The content (Record Data) for your chosen record type. For example, if --type A, --content should be an IPv4 IP. (required) --enabled When true - the record is visible for lookup (default true) diff --git a/docs/subcommands/DNS/secondary/zone/create.md b/docs/subcommands/DNS/secondary/zone/create.md new file mode 100644 index 000000000..311edb499 --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/create.md @@ -0,0 +1,57 @@ +--- +description: "Create a secondary zone" +--- + +# DnsSecondaryZoneCreate + +## Usage + +```text +ionosctl dns secondary-zone create [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +For `create` command: + +```text +[c] +``` + +## Description + +Create a new secondary zone with default NS and SOA records. Note that Cloud DNS relies on the following Anycast addresses for sending DNS notify messages. Make sure to whitelist on your end: + +IPv4: 212.227.123.25 +IPv6: 2001:8d8:fe:53::5cd:25 + +## 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 Description PrimaryIPs State] (default [Id,Name,Description,PrimaryIPs,State]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + --description string Description of the secondary zone + -f, --force Force command to execute without user input + -h, --help Print usage + -n, --name string Name of the secondary zone + --no-headers Don't print table headers when table output is used + -o, --output string Desired output format [text|json|api-json] (default "text") + --primary-ips strings Primary DNS server IP addresses + -q, --quiet Quiet output + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns secondary-zone create --name ZONE_NAME --description DESCRIPTION --primary-ips 1.2.3.4,5.6.7.8 +``` + diff --git a/docs/subcommands/DNS/secondary/zone/delete.md b/docs/subcommands/DNS/secondary/zone/delete.md new file mode 100644 index 000000000..e07ba7585 --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/delete.md @@ -0,0 +1,53 @@ +--- +description: "Delete a secondary zone" +--- + +# DnsSecondaryZoneDelete + +## Usage + +```text +ionosctl dns secondary-zone delete [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +For `delete` command: + +```text +[d del] +``` + +## Description + +Delete a secondary zone + +## Options + +```text + -a, --all Delete all secondary zones + -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 Description PrimaryIPs State] (default [Id,Name,Description,PrimaryIPs,State]) + -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 + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone +``` + +## Examples + +```text +ionosctl dns secondary-zone delete --zone ZONE_ID +``` + diff --git a/docs/subcommands/DNS/secondary/zone/get.md b/docs/subcommands/DNS/secondary/zone/get.md new file mode 100644 index 000000000..34667cfc7 --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/get.md @@ -0,0 +1,46 @@ +--- +description: "Retrieve a secondary zone" +--- + +# DnsSecondaryZoneGet + +## Usage + +```text +ionosctl dns secondary-zone get [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +## Description + +Retrieve a secondary zone by its ID or name + +## 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 Description PrimaryIPs State] (default [Id,Name,Description,PrimaryIPs,State]) + -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 + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone +``` + +## Examples + +```text +ionosctl dns secondary-zones get --zone ZONE_ID +``` + diff --git a/docs/subcommands/DNS/secondary/zone/list.md b/docs/subcommands/DNS/secondary/zone/list.md new file mode 100644 index 000000000..753f7e43b --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/list.md @@ -0,0 +1,49 @@ +--- +description: "List secondary zones" +--- + +# DnsSecondaryZoneList + +## Usage + +```text +ionosctl dns secondary-zone list [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +## Description + +List all secondary zones. Default limit is the first 100 items. Use pagination query parameters for listing more items (up to 1000). + +## 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 Description PrimaryIPs State] (default [Id,Name,Description,PrimaryIPs,State]) + -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 + --max-results int32 Pagination limit + -n, --name string Filter used to fetch only the zones that contain the specified zone name + --no-headers Don't print table headers when table output is used + --offset int32 Pagination offset + -o, --output string Desired output format [text|json|api-json] (default "text") + -q, --quiet Quiet output + --state string Filter used to fetch all zones in a particular state (AVAILABLE, FAILED, PROVISIONING, DESTROYING) + -v, --verbose Print step-by-step process when running command +``` + +## Examples + +```text +ionosctl dns secondary-zone list +``` + diff --git a/docs/subcommands/DNS/secondary/zone/transfer/get.md b/docs/subcommands/DNS/secondary/zone/transfer/get.md new file mode 100644 index 000000000..8b716574a --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/transfer/get.md @@ -0,0 +1,52 @@ +--- +description: "Get the transfer status for a secondary zone" +--- + +# DnsSecondaryZoneTransferGet + +## Usage + +```text +ionosctl dns secondary-zone transfer get [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +For `transfer` command: + +```text +[t] +``` + +For `get` command: + +```text +[g] +``` + +## Description + +Get the transfer status for a secondary zone + +## 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: [PrimaryIP Status ErrorMessage] (default [PrimaryIP,Status,ErrorMessage]) + -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 + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone +``` + diff --git a/docs/subcommands/DNS/secondary/zone/transfer/start.md b/docs/subcommands/DNS/secondary/zone/transfer/start.md new file mode 100644 index 000000000..690730df0 --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/transfer/start.md @@ -0,0 +1,52 @@ +--- +description: "Initiate zone transfer" +--- + +# DnsSecondaryZoneTransferStart + +## Usage + +```text +ionosctl dns secondary-zone transfer start [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +For `transfer` command: + +```text +[t] +``` + +For `start` command: + +```text +[s] +``` + +## Description + +Initiate zone transfer + +## 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: [PrimaryIP Status ErrorMessage] (default [PrimaryIP,Status,ErrorMessage]) + -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 + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone +``` + diff --git a/docs/subcommands/DNS/secondary/zone/update.md b/docs/subcommands/DNS/secondary/zone/update.md new file mode 100644 index 000000000..034fed8a7 --- /dev/null +++ b/docs/subcommands/DNS/secondary/zone/update.md @@ -0,0 +1,48 @@ +--- +description: "Update or create a secondary zone" +--- + +# DnsSecondaryZoneUpdate + +## Usage + +```text +ionosctl dns secondary-zone update [flags] +``` + +## Aliases + +For `secondary-zone` command: + +```text +[secondary-zones sz] +``` + +## Description + +Update or create a secondary zone + +## 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 Description PrimaryIPs State] (default [Id,Name,Description,PrimaryIPs,State]) + -c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json") + --description string Description of the secondary zone + -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") + --primary-ips strings Primary DNS server IP addresses + -q, --quiet Quiet output + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone +``` + +## Examples + +```text +ionosctl dns secondary-zone update --zone ZONE_ID --name ZONE_NAME --description DESCRIPTION --primary-ips 1.2.3.4,5.6.7.8 +``` + diff --git a/docs/subcommands/DNS/zone/file/get.md b/docs/subcommands/DNS/zone/file/get.md new file mode 100644 index 000000000..c9a8f15af --- /dev/null +++ b/docs/subcommands/DNS/zone/file/get.md @@ -0,0 +1,52 @@ +--- +description: "Get a specific zone file" +--- + +# DnsZoneFileGet + +## Usage + +```text +ionosctl dns zone file get [flags] +``` + +## Aliases + +For `zone` command: + +```text +[z zones] +``` + +For `file` command: + +```text +[f] +``` + +For `get` command: + +```text +[g] +``` + +## Description + +Get the exported zone file in BIND format (RFC 1035) + +## 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 Description NameServers Enabled State] + -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 + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone +``` + diff --git a/docs/subcommands/DNS/zone/file/update.md b/docs/subcommands/DNS/zone/file/update.md new file mode 100644 index 000000000..26e964b48 --- /dev/null +++ b/docs/subcommands/DNS/zone/file/update.md @@ -0,0 +1,53 @@ +--- +description: "Update a zone file" +--- + +# DnsZoneFileUpdate + +## Usage + +```text +ionosctl dns zone file update [flags] +``` + +## Aliases + +For `zone` command: + +```text +[z zones] +``` + +For `file` command: + +```text +[f] +``` + +## Description + +Update a zone file + +## 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 Description NameServers Enabled State] + -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 + -v, --verbose Print step-by-step process when running command + -z, --zone string The name or ID of the DNS zone + --zone-file string Path to the zone file +``` + +## Examples + +```text +ionosctl dns zone file update --zone ZONE_ID --zone-file FILE_PATH +``` + diff --git a/docs/summary.md b/docs/summary.md index ee16ef839..b8f97bb80 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -259,9 +259,22 @@ * [get](subcommands%2FDNS%2Freverse%2Frecord%2Fget.md) * [list](subcommands%2FDNS%2Freverse%2Frecord%2Flist.md) * [update](subcommands%2FDNS%2Freverse%2Frecord%2Fupdate.md) + * secondary + * zone + * [create](subcommands%2FDNS%2Fsecondary%2Fzone%2Fcreate.md) + * [delete](subcommands%2FDNS%2Fsecondary%2Fzone%2Fdelete.md) + * [get](subcommands%2FDNS%2Fsecondary%2Fzone%2Fget.md) + * [list](subcommands%2FDNS%2Fsecondary%2Fzone%2Flist.md) + * transfer + * [get](subcommands%2FDNS%2Fsecondary%2Fzone%2Ftransfer%2Fget.md) + * [start](subcommands%2FDNS%2Fsecondary%2Fzone%2Ftransfer%2Fstart.md) + * [update](subcommands%2FDNS%2Fsecondary%2Fzone%2Fupdate.md) * zone * [create](subcommands%2FDNS%2Fzone%2Fcreate.md) * [delete](subcommands%2FDNS%2Fzone%2Fdelete.md) + * file + * [get](subcommands%2FDNS%2Fzone%2Ffile%2Fget.md) + * [update](subcommands%2FDNS%2Fzone%2Ffile%2Fupdate.md) * [get](subcommands%2FDNS%2Fzone%2Fget.md) * [list](subcommands%2FDNS%2Fzone%2Flist.md) * [update](subcommands%2FDNS%2Fzone%2Fupdate.md) diff --git a/go.mod b/go.mod index 669e6059a..7e756e9da 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ionos-cloud/ionosctl/v6 -go 1.20 +go 1.23 require ( github.com/Jeffail/gabs/v2 v2.7.0 diff --git a/go.sum b/go.sum index 8594c7e22..4cb7df4ee 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,7 @@ github.com/fclairamb/ftpserver v0.6.0/go.mod h1:qpS9SluxSRBcYtfF7W4UObh9nKdMc9vK github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -184,6 +185,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -288,9 +290,11 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= @@ -400,6 +404,7 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -563,6 +568,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -819,6 +825,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14/go.mod h1:nzmlZQ+UqB5+55CRTV/dOaiK8OrPl6Co96Ob8lH4Wxw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 782e1fb10..aa94709d6 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -54,17 +54,20 @@ Within each layer, a token takes precedence over a username and password combina FlagAnnotationsShort = "A" FlagVersion = "version" - FlagZone = "zone" - FlagZoneShort = "z" - FlagRecord = "record" - FlagRecordShort = "r" - FlagState = "state" - FlagDescription = "description" - FlagEnabled = "enabled" - FlagContent = "content" - FlagTtl = "ttl" - FlagPriority = "priority" - FlagType = "type" + FlagZone = "zone" + FlagZoneShort = "z" + FlagRecord = "record" + FlagRecordShort = "r" + FlagState = "state" + FlagDescription = "description" + FlagEnabled = "enabled" + FlagContent = "content" + FlagTtl = "ttl" + FlagPriority = "priority" + FlagType = "type" + FlagPrimaryIPs = "primary-ips" + FlagZoneFile = "zone-file" + FlagSecondaryZone = "secondary-zone" FlagCloudInit = "cloud-init" FlagLoggingPipelineId = "pipeline-id" diff --git a/internal/printer/json2table/jsonpaths/dns.go b/internal/printer/json2table/jsonpaths/dns.go index 6f030f343..87f034dbb 100644 --- a/internal/printer/json2table/jsonpaths/dns.go +++ b/internal/printer/json2table/jsonpaths/dns.go @@ -16,8 +16,13 @@ var ( "Type": "properties.type", "Enabled": "properties.enabled", "FQDN": "metadata.fqdn", - "State": "metadata.state", - "ZoneId": "zoneId", + "ZoneId": "metadata.zoneId", + + // State is only in Zone Records + "State": "metadata.state", + + // RootName is only in Secondary Zone Records + "RootName": "metadata.rootName", } DnsZone = map[string]string{ @@ -50,4 +55,18 @@ var ( "Nsec3SaltBits": "properties.nsecParameters.nsec3SaltBits", "Validity": "properties.validity", } + + DnsSecondaryZone = map[string]string{ + "Id": "id", + "Name": "properties.zoneName", + "Description": "properties.description", + "PrimaryIPs": "properties.primaryIps", + "State": "metadata.state", + } + + DnsSecondaryZoneTransfer = map[string]string{ + "PrimaryIP": "primaryIp", + "Status": "status", + "ErrorMessage": "errorMessage", + } ) diff --git a/test/bats/dns/dns.bats b/test/bats/dns/dns.bats new file mode 100755 index 000000000..c54f8dad4 --- /dev/null +++ b/test/bats/dns/dns.bats @@ -0,0 +1,141 @@ +#!/usr/bin/env bats + +# tags: dns + +BATS_LIBS_PATH="${LIBS_PATH:-../libs}" # fallback to relative path if not set +load "${BATS_LIBS_PATH}/bats-assert/load" +load "${BATS_LIBS_PATH}/bats-support/load" +load '../setup.bats' + +setup_file() { + export IONOS_TOKEN=$(ionosctl token generate) + mkdir -p /tmp/bats_test +} + +@test "Create DNS Zone" { + run ionosctl dns zone create --name "cli-test-$(randStr 6).com" --enabled false -o json 2> /dev/null + assert_success + + zone_id=$(echo "$output" | jq -r '.id') + assert_regex "$zone_id" "$uuid_v4_regex" + + echo "created dns zone $zone_id" + echo "$zone_id" > /tmp/bats_test/zone_id +} + +@test "List and retrieve DNS Zone by ID" { + run ionosctl dns zone list -o json 2> /dev/null + assert_success + + zone_id=$(cat /tmp/bats_test/zone_id) + run ionosctl dns zone get --zone "${zone_id}" -o json 2> /dev/null + assert_success +} + +@test "Update DNS Zone" { + zone_id=$(cat /tmp/bats_test/zone_id) + run ionosctl dns zone update --zone "${zone_id}" --description "updated description" --enabled false -o json 2> /dev/null + assert_success +} + +@test "Create DNS Record in Zone" { + zone_id=$(cat /tmp/bats_test/zone_id) + run ionosctl dns record create --zone "${zone_id}" --name "test$(randStr 6)" --type A --content 1.2.3.4 -o json 2> /dev/null + assert_success + + record_id=$(echo "$output" | jq -r '.id') + assert_regex "$record_id" "$uuid_v4_regex" + + echo "created dns record $record_id" + echo "$record_id" > /tmp/bats_test/record_id +} + +@test "List and retrieve DNS Record by ID" { + zone_id=$(cat /tmp/bats_test/zone_id) + run ionosctl dns record list --zone "${zone_id}" -o json 2> /dev/null + assert_success + + record_count=$(echo "$output" | jq '.items' | jq length) + assert [ "$record_count" -gt 0 ] + echo "found $record_count records" + echo "$record_count" > /tmp/bats_test/record_count + + record_id=$(cat /tmp/bats_test/record_id) + run ionosctl dns record get --zone "${zone_id}" --record "${record_id}" -o json 2> /dev/null + assert_success +} + +@test "Update DNS Record" { + zone_id=$(cat /tmp/bats_test/zone_id) + record_id=$(cat /tmp/bats_test/record_id) + + run ionosctl dns record update --zone "${zone_id}" --record "${record_id}" --ttl 120 -o json 2> /dev/null + assert_success +} + +@test "Get and update DNS Zone File" { + zone_id=$(cat /tmp/bats_test/zone_id) + run ionosctl dns zone file get --zone "${zone_id}" -o text 2> /dev/null + assert_success + + echo "$output" > /tmp/bats_test/zone_file + echo "test$(randStr 6) 60 IN A 1.2.3.4" >> /tmp/bats_test/zone_file + + run ionosctl dns zone file update --zone "${zone_id}" --zone-file /tmp/bats_test/zone_file -o json 2> /dev/null + assert_success + + run ionosctl dns record list --zone "${zone_id}" -o json 2> /dev/null + assert_success + + record_count=$(echo "$output" | jq '.items' | jq length) + assert [ "$record_count" -gt "$(cat /tmp/bats_test/record_count)" ] +} + +@test "Create DNS Secondary Zone" { + run ionosctl dns secondary-zone create --name "cli-test-$(randStr 6).com" --primary-ips 1.2.3.4,5.6.7.8 -o json 2> /dev/null + assert_success + + zone_id=$(echo "$output" | jq -r '.id') + assert_regex "$zone_id" "$uuid_v4_regex" + + echo "created dns secondary zone $zone_id" + echo "$zone_id" > /tmp/bats_test/secondary_zone_id +} + +@test "List and retrieve DNS Secondary Zone by ID" { + run ionosctl dns secondary-zone list -o json 2> /dev/null + assert_success + + zone_id=$(cat /tmp/bats_test/secondary_zone_id) + run ionosctl dns secondary-zone get --zone "${zone_id}" -o json 2> /dev/null + assert_success +} + +@test "Update DNS Secondary Zone" { + zone_id=$(cat /tmp/bats_test/secondary_zone_id) + run ionosctl dns secondary-zone update --zone "${zone_id}" --description "updated description" -o json 2> /dev/null + assert_success +} + +@test "Start and check Transfer for Secondary Zone" { + zone_id=$(cat /tmp/bats_test/secondary_zone_id) + run ionosctl dns secondary-zone transfer start --zone "${zone_id}" -o json 2> /dev/null + assert_success + + run ionosctl dns secondary-zone transfer get --zone "${zone_id}" -o json 2> /dev/null + assert_success +} + +teardown_file() { + zone_id=$(cat /tmp/bats_test/zone_id) + secondary_zone_id=$(cat /tmp/bats_test/secondary_zone_id) + + echo "cleaning up dns zone $zone_id and secondary zone $secondary_zone_id with all related records" + run ionosctl dns secondary-zone delete --zone "$secondary_zone_id" -f + run ionosctl dns zone delete --zone "$zone_id" -f + + run ionosctl token delete --token "$IONOS_TOKEN" -f + unset IONOS_TOKEN + + rm -rf /tmp/bats_test +}