From 2d1872ec436925a4812093b35896e778ad5765ae Mon Sep 17 00:00:00 2001 From: Michele Giornetta Date: Wed, 27 May 2026 18:16:46 +0200 Subject: [PATCH 1/3] feat(domain): add domain record update command Implements `civo domain record update DOMAIN/DOMAIN_ID RECORD_ID` so records can be modified in place instead of remove-and-recreate. Only flags the user passes are changed; other fields keep their current values. Closes #386 --- cmd/domain/domain.go | 10 +++ cmd/domain/domain_record_update.go | 129 +++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 cmd/domain/domain_record_update.go diff --git a/cmd/domain/domain.go b/cmd/domain/domain.go index ee7c5d0f..ca241f9c 100644 --- a/cmd/domain/domain.go +++ b/cmd/domain/domain.go @@ -45,6 +45,7 @@ func init() { domainRecordCmd.AddCommand(domainRecordCreateCmd) domainRecordCmd.AddCommand(domainRecordShowCmd) domainRecordCmd.AddCommand(domainRecordRemoveCmd) + domainRecordCmd.AddCommand(domainRecordUpdateCmd) /* Flags for domain record create cmd @@ -55,4 +56,13 @@ func init() { domainRecordCreateCmd.Flags().IntVarP(&recordTTL, "ttl", "t", 600, "The TTL of the record") domainRecordCreateCmd.Flags().IntVarP(&recordPriority, "priority", "p", 0, "the priority of record only for SRV and MX record") + /* + Flags for domain record update cmd + */ + domainRecordUpdateCmd.Flags().StringVarP(&updateRecordName, "name", "n", "", "the name of the record") + domainRecordUpdateCmd.Flags().StringVarP(&updateRecordType, "type", "e", "", "type of the record (A, CNAME, TXT, SRV, MX, NS)") + domainRecordUpdateCmd.Flags().StringVarP(&updateRecordValue, "value", "v", "", "the value of the record") + domainRecordUpdateCmd.Flags().IntVarP(&updateRecordTTL, "ttl", "t", 0, "The TTL of the record") + domainRecordUpdateCmd.Flags().IntVarP(&updateRecordPriority, "priority", "p", 0, "the priority of record only for SRV and MX record") + } diff --git a/cmd/domain/domain_record_update.go b/cmd/domain/domain_record_update.go new file mode 100644 index 00000000..0889bc65 --- /dev/null +++ b/cmd/domain/domain_record_update.go @@ -0,0 +1,129 @@ +package domain + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var updateRecordName, updateRecordType, updateRecordValue string +var updateRecordTTL, updateRecordPriority int + +var domainRecordUpdateCmd = &cobra.Command{ + Use: "update [DOMAIN|DOMAIN_ID] [RECORD_ID]", + Aliases: []string{"change", "modify"}, + Short: "Update a domain record", + Args: cobra.MinimumNArgs(2), + Example: "civo domain record update DOMAIN/DOMAIN_ID RECORD_ID [flags]", + Run: func(cmd *cobra.Command, args []string) { + client, err := config.CivoAPIClient() + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + domain, err := client.FindDNSDomain(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s domain in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one domain with that name in your account") + os.Exit(1) + } + utility.Error("Unable to find the domain for your search %s", err) + os.Exit(1) + } + + record, err := client.GetDNSRecord(domain.ID, args[1]) + if err != nil { + if errors.Is(err, civogo.ErrDNSRecordNotFound) { + utility.Error("sorry there is no %s domain record in your account", utility.Red(args[1])) + os.Exit(1) + } + utility.Error("%s", err) + os.Exit(1) + } + + // Seed the config from the existing record so unspecified fields are preserved + // (the API call is a full-replace PUT). + recordConfig := &civogo.DNSRecordConfig{ + Type: record.Type, + Name: record.Name, + Value: record.Value, + TTL: record.TTL, + Priority: record.Priority, + } + + if cmd.Flags().Changed("name") { + recordConfig.Name = updateRecordName + } + + if cmd.Flags().Changed("value") { + recordConfig.Value = updateRecordValue + } + + if cmd.Flags().Changed("ttl") { + recordConfig.TTL = updateRecordTTL + } + + if cmd.Flags().Changed("priority") { + recordConfig.Priority = updateRecordPriority + } + + if cmd.Flags().Changed("type") { + // Sanitise the record type + updateRecordType = strings.ReplaceAll(updateRecordType, " ", "") + + if updateRecordType == "A" || updateRecordType == "a" || updateRecordType == "alias" { + recordConfig.Type = civogo.DNSRecordTypeA + } + + if updateRecordType == "CNAME" || updateRecordType == "cname" || updateRecordType == "canonical" { + recordConfig.Type = civogo.DNSRecordTypeCName + } + + if updateRecordType == "MX" || updateRecordType == "mx" || updateRecordType == "mail" { + recordConfig.Type = civogo.DNSRecordTypeMX + } + + if updateRecordType == "TXT" || updateRecordType == "txt" || updateRecordType == "text" { + recordConfig.Type = civogo.DNSRecordTypeTXT + } + + if updateRecordType == "SRV" || updateRecordType == "srv" || updateRecordType == "service" { + recordConfig.Type = civogo.DNSRecordTypeSRV + } + + if updateRecordType == "NS" || updateRecordType == "ns" || updateRecordType == "nameserver" { + recordConfig.Type = civogo.DNSRecordTypeNS + } + } + + updatedRecord, err := client.UpdateDNSRecord(record, recordConfig) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": updatedRecord.ID, "name": updatedRecord.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Updated %s record %s for %s with a TTL of %s seconds and with a priority of %s with ID %s\n", utility.Green(string(updatedRecord.Type)), utility.Green(updatedRecord.Name), utility.Green(domain.Name), utility.Green(strconv.Itoa(updatedRecord.TTL)), utility.Green(strconv.Itoa(updatedRecord.Priority)), utility.Green(updatedRecord.ID)) + } + }, +} From 7b3f6507fccb5134f3dd328915c8c5de923c702a Mon Sep 17 00:00:00 2001 From: Michele Giornetta Date: Wed, 27 May 2026 18:33:17 +0200 Subject: [PATCH 2/3] fix(domain): uppercase record type in update output --- cmd/domain/domain_record_update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/domain/domain_record_update.go b/cmd/domain/domain_record_update.go index 0889bc65..787f3107 100644 --- a/cmd/domain/domain_record_update.go +++ b/cmd/domain/domain_record_update.go @@ -123,7 +123,7 @@ var domainRecordUpdateCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("Updated %s record %s for %s with a TTL of %s seconds and with a priority of %s with ID %s\n", utility.Green(string(updatedRecord.Type)), utility.Green(updatedRecord.Name), utility.Green(domain.Name), utility.Green(strconv.Itoa(updatedRecord.TTL)), utility.Green(strconv.Itoa(updatedRecord.Priority)), utility.Green(updatedRecord.ID)) + fmt.Printf("Updated %s record %s for %s with a TTL of %s seconds and with a priority of %s with ID %s\n", utility.Green(strings.ToUpper(string(updatedRecord.Type))), utility.Green(updatedRecord.Name), utility.Green(domain.Name), utility.Green(strconv.Itoa(updatedRecord.TTL)), utility.Green(strconv.Itoa(updatedRecord.Priority)), utility.Green(updatedRecord.ID)) } }, } From 0fbc6063bb5c062fbd1bf6088c05b7487cb96072 Mon Sep 17 00:00:00 2001 From: Michele Giornetta Date: Wed, 27 May 2026 18:35:57 +0200 Subject: [PATCH 3/3] refactor(domain): consistent record output across commands Add a shared dnsRecordOutputWriter helper so show, create and update emit the full record field set in json/custom. Fixes create outputting the domain's id/name instead of the record's, fixes show using WriteMultipleObjectsJSON and missing the NS type case, and uppercases the record type consistently in list, create and update. --- cmd/domain/domain.go | 25 ++++++++++++++++++++ cmd/domain/domain_record_create.go | 4 ++-- cmd/domain/domain_record_list.go | 3 ++- cmd/domain/domain_record_show.go | 38 ++---------------------------- cmd/domain/domain_record_update.go | 2 +- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/cmd/domain/domain.go b/cmd/domain/domain.go index ca241f9c..d73610d3 100644 --- a/cmd/domain/domain.go +++ b/cmd/domain/domain.go @@ -2,7 +2,12 @@ package domain import ( "errors" + "strconv" + "strings" + "time" + "github.com/civo/civogo" + "github.com/civo/cli/utility" "github.com/spf13/cobra" ) @@ -66,3 +71,23 @@ func init() { domainRecordUpdateCmd.Flags().IntVarP(&updateRecordPriority, "priority", "p", 0, "the priority of record only for SRV and MX record") } + +// dnsRecordOutputWriter returns an OutputWriter populated with the full set of +// fields for a single DNS record, used by the show, create and update commands +// so their json/custom output stays consistent. +func dnsRecordOutputWriter(record *civogo.DNSRecord) *utility.OutputWriter { + ow := utility.NewOutputWriter() + ow.StartLine() + + ow.AppendDataWithLabel("id", record.ID, "ID") + ow.AppendDataWithLabel("domain_id", record.DNSDomainID, "Domain ID") + ow.AppendDataWithLabel("name", record.Name, "Name") + ow.AppendDataWithLabel("value", record.Value, "Value") + ow.AppendDataWithLabel("type", strings.ToUpper(string(record.Type)), "Type") + ow.AppendDataWithLabel("ttl", strconv.Itoa(record.TTL), "TTL") + ow.AppendDataWithLabel("priority", strconv.Itoa(record.Priority), "Priority") + ow.AppendDataWithLabel("created_at", record.CreatedAt.Format(time.RFC1123), "Created At") + ow.AppendDataWithLabel("updated_at", record.UpdatedAt.Format(time.RFC1123), "Updated At") + + return ow +} diff --git a/cmd/domain/domain_record_create.go b/cmd/domain/domain_record_create.go index 59522dba..d1fff72c 100644 --- a/cmd/domain/domain_record_create.go +++ b/cmd/domain/domain_record_create.go @@ -75,7 +75,7 @@ var domainRecordCreateCmd = &cobra.Command{ os.Exit(1) } - ow := utility.NewOutputWriterWithMap(map[string]string{"id": domain.ID, "name": domain.Name}) + ow := dnsRecordOutputWriter(record) switch common.OutputFormat { case "json": @@ -83,7 +83,7 @@ var domainRecordCreateCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("Created %s record %s for %s with a TTL of %s seconds and with a priority of %s with ID %s", utility.Green(string(record.Type)), utility.Green(record.Name), utility.Green(domain.Name), utility.Green(strconv.Itoa(record.TTL)), utility.Green(strconv.Itoa(record.Priority)), utility.Green(record.ID)) + fmt.Printf("Created %s record %s for %s with a TTL of %s seconds and with a priority of %s with ID %s\n", utility.Green(strings.ToUpper(string(record.Type))), utility.Green(record.Name), utility.Green(domain.Name), utility.Green(strconv.Itoa(record.TTL)), utility.Green(strconv.Itoa(record.Priority)), utility.Green(record.ID)) } }, } diff --git a/cmd/domain/domain_record_list.go b/cmd/domain/domain_record_list.go index 896a128a..4dbdeef7 100644 --- a/cmd/domain/domain_record_list.go +++ b/cmd/domain/domain_record_list.go @@ -3,6 +3,7 @@ package domain import ( "os" "strconv" + "strings" "github.com/civo/cli/config" "github.com/civo/cli/utility" @@ -56,7 +57,7 @@ If you wish to use a custom format, the available fields are: ow.AppendDataWithLabel("id", record.ID, "ID") ow.AppendDataWithLabel("name", record.Name, "Name") ow.AppendDataWithLabel("value", record.Value, "Value") - ow.AppendDataWithLabel("type", string(record.Type), "Type") + ow.AppendDataWithLabel("type", strings.ToUpper(string(record.Type)), "Type") ow.AppendDataWithLabel("ttl", strconv.Itoa(record.TTL), "TTL") ow.AppendDataWithLabel("priority", strconv.Itoa(record.Priority), "Priority") diff --git a/cmd/domain/domain_record_show.go b/cmd/domain/domain_record_show.go index 948cc332..0e289c42 100644 --- a/cmd/domain/domain_record_show.go +++ b/cmd/domain/domain_record_show.go @@ -1,14 +1,11 @@ package domain import ( - "github.com/civo/civogo" "github.com/civo/cli/common" "github.com/civo/cli/config" "github.com/civo/cli/utility" "os" - "strconv" - "time" "github.com/spf13/cobra" ) @@ -52,42 +49,11 @@ Example: civo domain record show RECORD_ID -o custom -f "ID: Name"`, os.Exit(1) } - ow := utility.NewOutputWriter() - ow.StartLine() - - ow.AppendDataWithLabel("id", record.ID, "ID") - ow.AppendDataWithLabel("domain_id", record.DNSDomainID, "Domain ID") - ow.AppendDataWithLabel("name", record.Name, "Name") - ow.AppendDataWithLabel("value", record.Value, "Value") - - if record.Type == "a" { - ow.AppendDataWithLabel("type", string(civogo.DNSRecordTypeA), "Type") - } - - if record.Type == "cname" { - ow.AppendDataWithLabel("type", string(civogo.DNSRecordTypeCName), "Type") - } - - if record.Type == "mx" { - ow.AppendDataWithLabel("type", string(civogo.DNSRecordTypeMX), "Type") - } - - if record.Type == "txt" { - ow.AppendDataWithLabel("type", string(civogo.DNSRecordTypeTXT), "Type") - } - - if record.Type == "srv" { - ow.AppendDataWithLabel("type", string(civogo.DNSRecordTypeSRV), "Type") - } - - ow.AppendDataWithLabel("ttl", strconv.Itoa(record.TTL), "TTL") - ow.AppendDataWithLabel("priority", strconv.Itoa(record.Priority), "Priority") - ow.AppendDataWithLabel("created_at", record.CreatedAt.Format(time.RFC1123), "Created At") - ow.AppendDataWithLabel("updated_at", record.UpdatedAt.Format(time.RFC1123), "Updated At") + ow := dnsRecordOutputWriter(record) switch common.OutputFormat { case "json": - ow.WriteMultipleObjectsJSON(common.PrettySet) + ow.WriteSingleObjectJSON(common.PrettySet) case "custom": ow.WriteCustomOutput(common.OutputFields) default: diff --git a/cmd/domain/domain_record_update.go b/cmd/domain/domain_record_update.go index 787f3107..57df78a0 100644 --- a/cmd/domain/domain_record_update.go +++ b/cmd/domain/domain_record_update.go @@ -115,7 +115,7 @@ var domainRecordUpdateCmd = &cobra.Command{ os.Exit(1) } - ow := utility.NewOutputWriterWithMap(map[string]string{"id": updatedRecord.ID, "name": updatedRecord.Name}) + ow := dnsRecordOutputWriter(updatedRecord) switch common.OutputFormat { case "json":