From 9e0a9350984fff381761eca3cafbd5ace8fe14a9 Mon Sep 17 00:00:00 2001 From: Gianluca Cannata Date: Tue, 3 Mar 2026 14:59:43 +0100 Subject: [PATCH 1/3] feat: add civo vpc command suite with full CRUD for VPC resources Implement the complete `civo vpc` command hierarchy using the VPC-prefixed methods from civogo SDK v0.7.0-alpha1. All commands target the /v2/vpc/ API path and follow existing CLI patterns for output formatting, batch deletion, region handling, and flag conventions. Command tree: civo vpc network ls | create | show | update | remove civo vpc subnet ls | create | show | remove | attach | detach civo vpc firewall ls | create | show | update | remove civo vpc firewall rule ls | create | remove civo vpc loadbalancer ls | create | show | update | remove civo vpc ip ls | reserve | show | update | assign | unassign | delete Implementation details: - 37 new files in cmd/vpc/ covering network, subnet, firewall, firewall rules, load balancer, and reserved IP resources - Wire VPCCmd into rootCmd via cmd/root.go - Subnet commands require --network flag to scope operations to a network - Load balancer create/update support repeatable --backend flag with format ip:source_port:target_port:protocol:health_check_port - IP assign uses --resource-id and --resource-type for generic resource assignment (not limited to instances) - Firewall list resolves network IDs to names via ListVPCNetworks - Firewall show includes inline rule listing - All remove commands support batch deletion with confirmation prompts - Three-way output support: json, custom fields, and human-readable --- cmd/root.go | 2 + cmd/vpc/vpc.go | 154 ++++++++++++++++++++++++++++ cmd/vpc/vpc_firewall.go | 20 ++++ cmd/vpc/vpc_firewall_create.go | 81 +++++++++++++++ cmd/vpc/vpc_firewall_list.go | 72 +++++++++++++ cmd/vpc/vpc_firewall_remove.go | 99 ++++++++++++++++++ cmd/vpc/vpc_firewall_rule.go | 20 ++++ cmd/vpc/vpc_firewall_rule_create.go | 123 ++++++++++++++++++++++ cmd/vpc/vpc_firewall_rule_list.go | 79 ++++++++++++++ cmd/vpc/vpc_firewall_rule_remove.go | 111 ++++++++++++++++++++ cmd/vpc/vpc_firewall_show.go | 77 ++++++++++++++ cmd/vpc/vpc_firewall_update.go | 58 +++++++++++ cmd/vpc/vpc_ip.go | 20 ++++ cmd/vpc/vpc_ip_assign.go | 68 ++++++++++++ cmd/vpc/vpc_ip_create.go | 62 +++++++++++ cmd/vpc/vpc_ip_list.go | 59 +++++++++++ cmd/vpc/vpc_ip_remove.go | 68 ++++++++++++ cmd/vpc/vpc_ip_show.go | 70 +++++++++++++ cmd/vpc/vpc_ip_unassign.go | 67 ++++++++++++ cmd/vpc/vpc_ip_update.go | 68 ++++++++++++ cmd/vpc/vpc_loadbalancer.go | 20 ++++ cmd/vpc/vpc_loadbalancer_create.go | 139 +++++++++++++++++++++++++ cmd/vpc/vpc_loadbalancer_list.go | 74 +++++++++++++ cmd/vpc/vpc_loadbalancer_remove.go | 99 ++++++++++++++++++ cmd/vpc/vpc_loadbalancer_show.go | 97 ++++++++++++++++++ cmd/vpc/vpc_loadbalancer_update.go | 104 +++++++++++++++++++ cmd/vpc/vpc_network.go | 20 ++++ cmd/vpc/vpc_network_create.go | 78 ++++++++++++++ cmd/vpc/vpc_network_list.go | 63 ++++++++++++ cmd/vpc/vpc_network_remove.go | 109 ++++++++++++++++++++ cmd/vpc/vpc_network_show.go | 76 ++++++++++++++ cmd/vpc/vpc_network_update.go | 54 ++++++++++ cmd/vpc/vpc_subnet.go | 20 ++++ cmd/vpc/vpc_subnet_attach.go | 70 +++++++++++++ cmd/vpc/vpc_subnet_create.go | 59 +++++++++++ cmd/vpc/vpc_subnet_detach.go | 61 +++++++++++ cmd/vpc/vpc_subnet_list.go | 64 ++++++++++++ cmd/vpc/vpc_subnet_remove.go | 107 +++++++++++++++++++ cmd/vpc/vpc_subnet_show.go | 67 ++++++++++++ go.mod | 4 +- go.sum | 4 +- 41 files changed, 2763 insertions(+), 4 deletions(-) create mode 100644 cmd/vpc/vpc.go create mode 100644 cmd/vpc/vpc_firewall.go create mode 100644 cmd/vpc/vpc_firewall_create.go create mode 100644 cmd/vpc/vpc_firewall_list.go create mode 100644 cmd/vpc/vpc_firewall_remove.go create mode 100644 cmd/vpc/vpc_firewall_rule.go create mode 100644 cmd/vpc/vpc_firewall_rule_create.go create mode 100644 cmd/vpc/vpc_firewall_rule_list.go create mode 100644 cmd/vpc/vpc_firewall_rule_remove.go create mode 100644 cmd/vpc/vpc_firewall_show.go create mode 100644 cmd/vpc/vpc_firewall_update.go create mode 100644 cmd/vpc/vpc_ip.go create mode 100644 cmd/vpc/vpc_ip_assign.go create mode 100644 cmd/vpc/vpc_ip_create.go create mode 100644 cmd/vpc/vpc_ip_list.go create mode 100644 cmd/vpc/vpc_ip_remove.go create mode 100644 cmd/vpc/vpc_ip_show.go create mode 100644 cmd/vpc/vpc_ip_unassign.go create mode 100644 cmd/vpc/vpc_ip_update.go create mode 100644 cmd/vpc/vpc_loadbalancer.go create mode 100644 cmd/vpc/vpc_loadbalancer_create.go create mode 100644 cmd/vpc/vpc_loadbalancer_list.go create mode 100644 cmd/vpc/vpc_loadbalancer_remove.go create mode 100644 cmd/vpc/vpc_loadbalancer_show.go create mode 100644 cmd/vpc/vpc_loadbalancer_update.go create mode 100644 cmd/vpc/vpc_network.go create mode 100644 cmd/vpc/vpc_network_create.go create mode 100644 cmd/vpc/vpc_network_list.go create mode 100644 cmd/vpc/vpc_network_remove.go create mode 100644 cmd/vpc/vpc_network_show.go create mode 100644 cmd/vpc/vpc_network_update.go create mode 100644 cmd/vpc/vpc_subnet.go create mode 100644 cmd/vpc/vpc_subnet_attach.go create mode 100644 cmd/vpc/vpc_subnet_create.go create mode 100644 cmd/vpc/vpc_subnet_detach.go create mode 100644 cmd/vpc/vpc_subnet_list.go create mode 100644 cmd/vpc/vpc_subnet_remove.go create mode 100644 cmd/vpc/vpc_subnet_show.go diff --git a/cmd/root.go b/cmd/root.go index 0fb92e49..63e4a216 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,6 +24,7 @@ import ( "github.com/civo/cli/cmd/teams" "github.com/civo/cli/cmd/volume" "github.com/civo/cli/cmd/volumetype" + "github.com/civo/cli/cmd/vpc" "github.com/civo/cli/common" "github.com/civo/cli/config" "github.com/civo/cli/utility" @@ -163,4 +164,5 @@ func init() { rootCmd.AddCommand(volume.VolumeCmd) rootCmd.AddCommand(volumetype.VolumeTypeCmd) rootCmd.AddCommand(snapshot.SnapshotCmd) + rootCmd.AddCommand(vpc.VPCCmd) } diff --git a/cmd/vpc/vpc.go b/cmd/vpc/vpc.go new file mode 100644 index 00000000..0c640011 --- /dev/null +++ b/cmd/vpc/vpc.go @@ -0,0 +1,154 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +// VPCCmd manages Civo VPC resources +var VPCCmd = &cobra.Command{ + Use: "vpc", + Aliases: []string{}, + Short: "Details of Civo VPC resources", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} + +func init() { + // Sub-resource commands + VPCCmd.AddCommand(vpcNetworkCmd) + VPCCmd.AddCommand(vpcSubnetCmd) + VPCCmd.AddCommand(vpcFirewallCmd) + VPCCmd.AddCommand(vpcLoadBalancerCmd) + VPCCmd.AddCommand(vpcIPCmd) + + // --- Network subcommands --- + vpcNetworkCmd.AddCommand(vpcNetworkListCmd) + vpcNetworkCmd.AddCommand(vpcNetworkCreateCmd) + vpcNetworkCmd.AddCommand(vpcNetworkShowCmd) + vpcNetworkCmd.AddCommand(vpcNetworkUpdateCmd) + vpcNetworkCmd.AddCommand(vpcNetworkRemoveCmd) + + // Network create flags + vpcNetworkCreateCmd.Flags().StringVarP(&vpcNetCIDRv4, "cidr-v4", "", "", "Custom IPv4 CIDR") + vpcNetworkCreateCmd.Flags().StringSliceVarP(&vpcNetNameserversV4, "nameservers-v4", "", nil, "Custom list of IPv4 nameservers (comma-separated)") + vpcNetworkCreateCmd.Flags().BoolVarP(&vpcNetIPv4Enabled, "ipv4-enabled", "", true, "Enable IPv4 for the network") + vpcNetworkCreateCmd.Flags().BoolVarP(&vpcNetIPv6Enabled, "ipv6-enabled", "", false, "Enable IPv6 for the network") + vpcNetworkCreateCmd.Flags().StringSliceVarP(&vpcNetNameserversV6, "nameservers-v6", "", nil, "Custom list of IPv6 nameservers (comma-separated)") + + // --- Subnet subcommands --- + vpcSubnetCmd.AddCommand(vpcSubnetListCmd) + vpcSubnetCmd.AddCommand(vpcSubnetCreateCmd) + vpcSubnetCmd.AddCommand(vpcSubnetShowCmd) + vpcSubnetCmd.AddCommand(vpcSubnetRemoveCmd) + vpcSubnetCmd.AddCommand(vpcSubnetAttachCmd) + vpcSubnetCmd.AddCommand(vpcSubnetDetachCmd) + + // Subnet flags (all require --network) + vpcSubnetListCmd.Flags().StringVarP(&subnetListNetworkID, "network", "n", "", "Network name or ID") + _ = vpcSubnetListCmd.MarkFlagRequired("network") + + vpcSubnetCreateCmd.Flags().StringVarP(&subnetCreateNetworkID, "network", "n", "", "Network name or ID") + _ = vpcSubnetCreateCmd.MarkFlagRequired("network") + + vpcSubnetShowCmd.Flags().StringVarP(&subnetShowNetworkID, "network", "n", "", "Network name or ID") + _ = vpcSubnetShowCmd.MarkFlagRequired("network") + + vpcSubnetRemoveCmd.Flags().StringVarP(&subnetRemoveNetworkID, "network", "n", "", "Network name or ID") + _ = vpcSubnetRemoveCmd.MarkFlagRequired("network") + + vpcSubnetAttachCmd.Flags().StringVarP(&subnetAttachNetworkID, "network", "n", "", "Network name or ID") + _ = vpcSubnetAttachCmd.MarkFlagRequired("network") + vpcSubnetAttachCmd.Flags().StringVar(&subnetAttachResourceID, "resource-id", "", "Resource ID to attach to") + _ = vpcSubnetAttachCmd.MarkFlagRequired("resource-id") + vpcSubnetAttachCmd.Flags().StringVar(&subnetAttachResourceType, "resource-type", "", "Resource type (e.g. instance)") + _ = vpcSubnetAttachCmd.MarkFlagRequired("resource-type") + + vpcSubnetDetachCmd.Flags().StringVarP(&subnetDetachNetworkID, "network", "n", "", "Network name or ID") + _ = vpcSubnetDetachCmd.MarkFlagRequired("network") + + // --- Firewall subcommands --- + vpcFirewallCmd.AddCommand(vpcFirewallListCmd) + vpcFirewallCmd.AddCommand(vpcFirewallCreateCmd) + vpcFirewallCmd.AddCommand(vpcFirewallShowCmd) + vpcFirewallCmd.AddCommand(vpcFirewallUpdateCmd) + vpcFirewallCmd.AddCommand(vpcFirewallRemoveCmd) + + // Firewall create flags + vpcFirewallCreateCmd.Flags().StringVarP(&vpcFwNetwork, "network", "n", "default", "The network to create the firewall in") + vpcFirewallCreateCmd.Flags().BoolVarP(&vpcFwNoDefaultRules, "no-default-rules", "", false, "Create firewall without default rules") + + // --- Firewall Rule subcommands --- + vpcFirewallCmd.AddCommand(vpcFirewallRuleCmd) + vpcFirewallRuleCmd.AddCommand(vpcFirewallRuleListCmd) + vpcFirewallRuleCmd.AddCommand(vpcFirewallRuleCreateCmd) + vpcFirewallRuleCmd.AddCommand(vpcFirewallRuleRemoveCmd) + + // Firewall rule create flags + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleProtocol, "protocol", "p", "TCP", "The protocol choice (TCP, UDP, ICMP)") + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleStartPort, "startport", "s", "", "The start port of the rule") + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleEndPort, "endport", "e", "", "The end port of the rule") + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleCidr, "cidr", "c", "0.0.0.0/0", "The CIDR of the rule") + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleDirection, "direction", "d", "ingress", "The direction of the rule (ingress or egress)") + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleAction, "action", "a", "allow", "The action of the rule (allow or deny)") + vpcFirewallRuleCreateCmd.Flags().StringVarP(&vpcFwRuleLabel, "label", "l", "", "A label for this rule") + _ = vpcFirewallRuleCreateCmd.MarkFlagRequired("startport") + + // --- Load Balancer subcommands --- + vpcLoadBalancerCmd.AddCommand(vpcLoadBalancerListCmd) + vpcLoadBalancerCmd.AddCommand(vpcLoadBalancerCreateCmd) + vpcLoadBalancerCmd.AddCommand(vpcLoadBalancerShowCmd) + vpcLoadBalancerCmd.AddCommand(vpcLoadBalancerUpdateCmd) + vpcLoadBalancerCmd.AddCommand(vpcLoadBalancerRemoveCmd) + + // Load balancer create flags + vpcLoadBalancerCreateCmd.Flags().StringVarP(&vpcLBNetwork, "network", "n", "", "Network name or ID") + _ = vpcLoadBalancerCreateCmd.MarkFlagRequired("network") + vpcLoadBalancerCreateCmd.Flags().StringVar(&vpcLBAlgorithm, "algorithm", "round_robin", "Load balancing algorithm") + vpcLoadBalancerCreateCmd.Flags().StringVar(&vpcLBExternalTrafficPolicy, "external-traffic-policy", "", "External traffic policy") + vpcLoadBalancerCreateCmd.Flags().StringVar(&vpcLBSessionAffinity, "session-affinity", "", "Session affinity") + vpcLoadBalancerCreateCmd.Flags().Int32Var(&vpcLBSessionAffinityTimeout, "session-affinity-timeout", 0, "Session affinity timeout in seconds") + vpcLoadBalancerCreateCmd.Flags().StringVar(&vpcLBEnableProxyProtocol, "enable-proxy-protocol", "", "Enable proxy protocol") + vpcLoadBalancerCreateCmd.Flags().StringVar(&vpcLBFirewallID, "firewall-id", "", "Firewall ID to associate") + vpcLoadBalancerCreateCmd.Flags().IntVar(&vpcLBMaxConcurrentRequests, "max-concurrent-requests", 0, "Maximum concurrent requests") + vpcLoadBalancerCreateCmd.Flags().StringSliceVar(&vpcLBBackends, "backend", nil, "Backend in format ip:source_port:target_port:protocol:health_check_port (repeatable)") + + // Load balancer update flags + vpcLoadBalancerUpdateCmd.Flags().StringVar(&vpcLBUpdateName, "name", "", "New name for the load balancer") + vpcLoadBalancerUpdateCmd.Flags().StringVar(&vpcLBUpdateAlgorithm, "algorithm", "", "Load balancing algorithm") + vpcLoadBalancerUpdateCmd.Flags().StringVar(&vpcLBUpdateExternalTrafficPolicy, "external-traffic-policy", "", "External traffic policy") + vpcLoadBalancerUpdateCmd.Flags().StringVar(&vpcLBUpdateSessionAffinity, "session-affinity", "", "Session affinity") + vpcLoadBalancerUpdateCmd.Flags().Int32Var(&vpcLBUpdateSessionAffinityTimeout, "session-affinity-timeout", 0, "Session affinity timeout in seconds") + vpcLoadBalancerUpdateCmd.Flags().StringVar(&vpcLBUpdateEnableProxyProtocol, "enable-proxy-protocol", "", "Enable proxy protocol") + vpcLoadBalancerUpdateCmd.Flags().StringVar(&vpcLBUpdateFirewallID, "firewall-id", "", "Firewall ID to associate") + vpcLoadBalancerUpdateCmd.Flags().IntVar(&vpcLBUpdateMaxConcurrentRequests, "max-concurrent-requests", 0, "Maximum concurrent requests") + vpcLoadBalancerUpdateCmd.Flags().StringSliceVar(&vpcLBUpdateBackends, "backend", nil, "Backend in format ip:source_port:target_port:protocol:health_check_port (repeatable)") + + // --- IP subcommands --- + vpcIPCmd.AddCommand(vpcIPListCmd) + vpcIPCmd.AddCommand(vpcIPCreateCmd) + vpcIPCmd.AddCommand(vpcIPShowCmd) + vpcIPCmd.AddCommand(vpcIPUpdateCmd) + vpcIPCmd.AddCommand(vpcIPAssignCmd) + vpcIPCmd.AddCommand(vpcIPUnassignCmd) + vpcIPCmd.AddCommand(vpcIPRemoveCmd) + + // IP create flags + vpcIPCreateCmd.Flags().StringVarP(&vpcIPName, "name", "n", "", "Name of the reserved IP") + + // IP update flags + vpcIPUpdateCmd.Flags().StringVarP(&vpcIPUpdateName, "name", "n", "", "New name for the reserved IP") + _ = vpcIPUpdateCmd.MarkFlagRequired("name") + + // IP assign flags + vpcIPAssignCmd.Flags().StringVar(&vpcIPAssignResourceID, "resource-id", "", "Resource ID to assign the IP to") + _ = vpcIPAssignCmd.MarkFlagRequired("resource-id") + vpcIPAssignCmd.Flags().StringVar(&vpcIPAssignResourceType, "resource-type", "", "Resource type (e.g. instance, loadbalancer)") + _ = vpcIPAssignCmd.MarkFlagRequired("resource-type") +} diff --git a/cmd/vpc/vpc_firewall.go b/cmd/vpc/vpc_firewall.go new file mode 100644 index 00000000..fb9484c6 --- /dev/null +++ b/cmd/vpc/vpc_firewall.go @@ -0,0 +1,20 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var vpcFirewallCmd = &cobra.Command{ + Use: "firewall", + Aliases: []string{"firewalls", "fw"}, + Short: "Details of Civo VPC firewalls", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} diff --git a/cmd/vpc/vpc_firewall_create.go b/cmd/vpc/vpc_firewall_create.go new file mode 100644 index 00000000..cf3cf828 --- /dev/null +++ b/cmd/vpc/vpc_firewall_create.go @@ -0,0 +1,81 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var ( + vpcFwNetwork string + vpcFwNoDefaultRules bool +) + +var vpcFirewallCreateCmd = &cobra.Command{ + Use: "create", + Aliases: []string{"new", "add"}, + Short: "Create a new VPC firewall", + Example: "civo vpc firewall create NAME [flags]", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + var networkID string + if vpcFwNetwork == "default" { + defaultNetwork, err := client.GetDefaultVPCNetwork() + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + networkID = defaultNetwork.ID + } else { + network, err := client.FindVPCNetwork(vpcFwNetwork) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + networkID = network.ID + } + + createRules := true + if vpcFwNoDefaultRules { + createRules = false + } + + firewall, err := client.NewVPCFirewall(&civogo.FirewallConfig{ + Name: args[0], + NetworkID: networkID, + CreateRules: &createRules, + Region: client.Region, + }) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": firewall.ID, "name": firewall.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Created a VPC firewall called %s with ID %s\n", utility.Green(firewall.Name), utility.Green(firewall.ID)) + } + }, +} diff --git a/cmd/vpc/vpc_firewall_list.go b/cmd/vpc/vpc_firewall_list.go new file mode 100644 index 00000000..ac6876c8 --- /dev/null +++ b/cmd/vpc/vpc_firewall_list.go @@ -0,0 +1,72 @@ +package vpc + +import ( + "os" + "strconv" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcFirewallListCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list", "all"}, + Short: "List VPC firewalls", + Long: `List all current VPC firewalls. +If you wish to use a custom format, the available fields are: + + * id + * name + * network + * rules_count + * instances_count + * clusters_count + * loadbalancer_count + +Example: civo vpc firewall ls -o custom -f "ID: Name"`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + firewalls, err := client.ListVPCFirewalls() + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + networks, err := client.ListVPCNetworks() + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + networkMap := make(map[string]string) + for _, network := range networks { + networkMap[network.ID] = network.Label + } + + ow := utility.NewOutputWriter() + for _, firewall := range firewalls { + ow.StartLine() + ow.AppendDataWithLabel("id", firewall.ID, "ID") + ow.AppendDataWithLabel("name", firewall.Name, "Name") + ow.AppendDataWithLabel("network", networkMap[firewall.NetworkID], "Network") + ow.AppendDataWithLabel("rules_count", strconv.Itoa(firewall.RulesCount), "Total rules") + ow.AppendDataWithLabel("instances_count", strconv.Itoa(firewall.InstanceCount), "Total Instances") + ow.AppendDataWithLabel("clusters_count", strconv.Itoa(firewall.ClusterCount), "Total Clusters") + ow.AppendDataWithLabel("loadbalancer_count", strconv.Itoa(firewall.LoadBalancerCount), "Total LoadBalancer") + } + + ow.FinishAndPrintOutput() + }, +} diff --git a/cmd/vpc/vpc_firewall_remove.go b/cmd/vpc/vpc_firewall_remove.go new file mode 100644 index 00000000..92c4a3aa --- /dev/null +++ b/cmd/vpc/vpc_firewall_remove.go @@ -0,0 +1,99 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/pkg/pluralize" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcFirewallResourceList []utility.Resource +var vpcFirewallRemoveCmd = &cobra.Command{ + Use: "remove [NAME]", + Aliases: []string{"rm", "delete", "destroy"}, + Example: "civo vpc firewall remove NAME", + Short: "Remove a VPC firewall", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + if len(args) == 1 { + firewall, err := client.FindVPCFirewall(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC firewall in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC firewall with that name in your account") + os.Exit(1) + } + } + vpcFirewallResourceList = append(vpcFirewallResourceList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + } else { + for _, v := range args { + firewall, err := client.FindVPCFirewall(v) + if err == nil { + vpcFirewallResourceList = append(vpcFirewallResourceList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + } + } + } + + firewallNameList := []string{} + for _, v := range vpcFirewallResourceList { + firewallNameList = append(firewallNameList, v.Name) + } + + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(vpcFirewallResourceList), "VPC firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { + for _, v := range vpcFirewallResourceList { + _, err = client.DeleteVPCFirewall(v.ID) + if err != nil { + utility.Error("error deleting the VPC firewall: %s", err) + os.Exit(1) + } + } + + ow := utility.NewOutputWriter() + for _, v := range vpcFirewallResourceList { + ow.StartLine() + ow.AppendDataWithLabel("id", v.ID, "ID") + ow.AppendDataWithLabel("name", v.Name, "Name") + } + + switch common.OutputFormat { + case "json": + if len(vpcFirewallResourceList) == 1 { + ow.WriteSingleObjectJSON(common.PrettySet) + } else { + ow.WriteMultipleObjectsJSON(common.PrettySet) + } + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("The %s (%s) %s been deleted\n", + pluralize.Pluralize(len(vpcFirewallResourceList), "VPC firewall"), + utility.Green(strings.Join(firewallNameList, ", ")), + pluralize.Has(len(vpcFirewallResourceList)), + ) + } + } else { + fmt.Println("Operation aborted.") + } + }, +} diff --git a/cmd/vpc/vpc_firewall_rule.go b/cmd/vpc/vpc_firewall_rule.go new file mode 100644 index 00000000..ecdbc20c --- /dev/null +++ b/cmd/vpc/vpc_firewall_rule.go @@ -0,0 +1,20 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var vpcFirewallRuleCmd = &cobra.Command{ + Use: "rule", + Aliases: []string{"rules"}, + Short: "Details of Civo VPC firewall rules", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} diff --git a/cmd/vpc/vpc_firewall_rule_create.go b/cmd/vpc/vpc_firewall_rule_create.go new file mode 100644 index 00000000..123e71c1 --- /dev/null +++ b/cmd/vpc/vpc_firewall_rule_create.go @@ -0,0 +1,123 @@ +package vpc + +import ( + "fmt" + "net" + "os" + "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 ( + vpcFwRuleProtocol string + vpcFwRuleStartPort string + vpcFwRuleEndPort string + vpcFwRuleCidr string + vpcFwRuleDirection string + vpcFwRuleAction string + vpcFwRuleLabel string +) + +var vpcFirewallRuleCreateCmd = &cobra.Command{ + Use: "create", + Aliases: []string{"new", "add"}, + Short: "Create a new VPC firewall rule", + Args: cobra.MinimumNArgs(1), + Example: "civo vpc firewall rule create FIREWALL_NAME/FIREWALL_ID [flags]", + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + firewall, err := client.FindVPCFirewall(args[0]) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + if err := vpcValidateCIDRs(vpcFwRuleCidr); err != nil { + utility.Error("%s", err.Error()) + os.Exit(1) + } + + newRuleConfig := &civogo.FirewallRuleConfig{ + FirewallID: firewall.ID, + Protocol: vpcFwRuleProtocol, + StartPort: vpcFwRuleStartPort, + Cidr: strings.Split(vpcFwRuleCidr, ","), + Label: vpcFwRuleLabel, + Action: vpcFwRuleAction, + Region: client.Region, + } + + var directionValue string + switch vpcFwRuleDirection { + case "ingress": + newRuleConfig.Direction = vpcFwRuleDirection + directionValue = "from" + case "egress": + newRuleConfig.Direction = vpcFwRuleDirection + directionValue = "to" + default: + utility.Error("'--direction' flag must be 'ingress' or 'egress'") + os.Exit(1) + } + + if vpcFwRuleEndPort == "" { + newRuleConfig.EndPort = vpcFwRuleStartPort + } else { + newRuleConfig.EndPort = vpcFwRuleEndPort + } + + rule, err := client.NewVPCFirewallRule(newRuleConfig) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": rule.ID, "name": rule.Label}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + if rule.Label == "" { + fmt.Printf("Created a VPC firewall rule to %s, %s access to port %s %s %s with ID %s\n", + utility.Green(newRuleConfig.Action), utility.Green(newRuleConfig.Direction), + utility.Green(newRuleConfig.StartPort), directionValue, + utility.Green(strings.Join(newRuleConfig.Cidr, ", ")), rule.ID) + } else { + fmt.Printf("Created a VPC firewall rule called %s to %s, %s access to port %s %s %s with ID %s\n", + utility.Green(rule.Label), utility.Green(newRuleConfig.Action), + utility.Green(newRuleConfig.Direction), utility.Green(newRuleConfig.StartPort), + directionValue, utility.Green(strings.Join(newRuleConfig.Cidr, ", ")), rule.ID) + } + } + }, +} + +func vpcValidateCIDRs(cidrs string) error { + for _, cidr := range strings.Split(cidrs, ",") { + if cidr = strings.TrimSpace(cidr); cidr == "" { + continue + } + if _, _, err := net.ParseCIDR(cidr); err != nil { + return fmt.Errorf("invalid CIDR address '%s': %s", cidr, err) + } + } + return nil +} diff --git a/cmd/vpc/vpc_firewall_rule_list.go b/cmd/vpc/vpc_firewall_rule_list.go new file mode 100644 index 00000000..1e543abd --- /dev/null +++ b/cmd/vpc/vpc_firewall_rule_list.go @@ -0,0 +1,79 @@ +package vpc + +import ( + "os" + "strings" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcFirewallRuleListCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list", "all"}, + Args: cobra.MinimumNArgs(1), + Example: "civo vpc firewall rule ls FIREWALL_NAME", + Short: "List VPC firewall rules", + Long: `List all current VPC firewall rules. +If you wish to use a custom format, the available fields are: + + * id + * direction + * action + * protocol + * start_port + * end_port + * cidr + * label`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + firewall, err := client.FindVPCFirewall(args[0]) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + firewallRules, err := client.ListVPCFirewallRules(firewall.ID) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + if len(firewallRules) == 0 { + utility.Info("%s VPC firewall has no rules", firewall.Name) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + for _, firewallRule := range firewallRules { + ow.StartLine() + + if firewallRule.EndPort == "" && firewallRule.StartPort != "" { + firewallRule.EndPort = firewallRule.StartPort + } + + ow.AppendDataWithLabel("id", firewallRule.ID, "ID") + ow.AppendDataWithLabel("direction", firewallRule.Direction, "Direction") + ow.AppendDataWithLabel("protocol", firewallRule.Protocol, "Protocol") + ow.AppendDataWithLabel("start_port", firewallRule.StartPort, "Start Port") + ow.AppendDataWithLabel("end_port", firewallRule.EndPort, "End Port") + ow.AppendDataWithLabel("action", firewallRule.Action, "Action") + ow.AppendDataWithLabel("cidr", strings.Join(firewallRule.Cidr, ", "), "Cidr") + ow.AppendDataWithLabel("label", firewallRule.Label, "Label") + } + + ow.FinishAndPrintOutput() + }, +} diff --git a/cmd/vpc/vpc_firewall_rule_remove.go b/cmd/vpc/vpc_firewall_rule_remove.go new file mode 100644 index 00000000..c0f1a9e7 --- /dev/null +++ b/cmd/vpc/vpc_firewall_rule_remove.go @@ -0,0 +1,111 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/pkg/pluralize" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcFirewallRuleResourceList []utility.Resource +var vpcFirewallRuleRemoveCmd = &cobra.Command{ + Use: "remove", + Aliases: []string{"delete", "destroy", "rm"}, + Args: cobra.MinimumNArgs(2), + Short: "Remove VPC firewall rule", + Example: "civo vpc firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + firewall, err := client.FindVPCFirewall(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC firewall in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC firewall with that name in your account") + os.Exit(1) + } + } + + if len(args) == 2 { + rule, err := client.FindVPCFirewallRule(firewall.ID, args[1]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC firewall rule in your account", utility.Red(args[1])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC firewall rule in your account") + os.Exit(1) + } + } + vpcFirewallRuleResourceList = append(vpcFirewallRuleResourceList, utility.Resource{ID: rule.ID, Name: rule.Label}) + } else { + for _, v := range args[1:] { + rule, err := client.FindVPCFirewallRule(firewall.ID, v) + if err == nil { + vpcFirewallRuleResourceList = append(vpcFirewallRuleResourceList, utility.Resource{ID: rule.ID, Name: rule.Label}) + } + } + } + + firewallRuleNameList := []string{} + for _, v := range vpcFirewallRuleResourceList { + firewallRuleNameList = append(firewallRuleNameList, v.Name) + } + + if utility.UserConfirmedDeletion(fmt.Sprintf("VPC firewall %s", pluralize.Pluralize(len(vpcFirewallRuleResourceList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { + for _, v := range vpcFirewallRuleResourceList { + _, err = client.DeleteVPCFirewallRule(firewall.ID, v.ID) + if err != nil { + utility.Error("error deleting the VPC firewall rule: %s", err) + os.Exit(1) + } + } + + ow := utility.NewOutputWriter() + for _, v := range vpcFirewallRuleResourceList { + ow.StartLine() + ow.AppendDataWithLabel("id", v.ID, "ID") + ow.AppendDataWithLabel("label", v.Name, "Label") + } + + switch common.OutputFormat { + case "json": + if len(vpcFirewallRuleResourceList) == 1 { + ow.WriteSingleObjectJSON(common.PrettySet) + } else { + ow.WriteMultipleObjectsJSON(common.PrettySet) + } + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("The VPC firewall %s (%s) %s been deleted\n", + pluralize.Pluralize(len(vpcFirewallRuleResourceList), "rule"), + strings.Join(firewallRuleNameList, ", "), + pluralize.Has(len(vpcFirewallRuleResourceList)), + ) + } + } else { + fmt.Println("Operation aborted.") + } + }, +} diff --git a/cmd/vpc/vpc_firewall_show.go b/cmd/vpc/vpc_firewall_show.go new file mode 100644 index 00000000..ffcf56c5 --- /dev/null +++ b/cmd/vpc/vpc_firewall_show.go @@ -0,0 +1,77 @@ +package vpc + +import ( + "fmt" + "os" + "strings" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcFirewallShowCmd = &cobra.Command{ + Use: "show [FIREWALL-NAME/FIREWALL-ID]", + Short: "Show details of a specific VPC firewall", + Aliases: []string{"get", "describe", "inspect"}, + Args: cobra.ExactArgs(1), + Example: "civo vpc firewall show FIREWALL_NAME", + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + firewall, err := client.FindVPCFirewall(args[0]) + if err != nil { + utility.Error("Firewall %s", err) + os.Exit(1) + } + + rules, err := client.ListVPCFirewallRules(firewall.ID) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + ow.StartLine() + ow.AppendDataWithLabel("id", firewall.ID, "ID") + ow.AppendDataWithLabel("name", firewall.Name, "Name") + ow.AppendDataWithLabel("network_id", firewall.NetworkID, "Network ID") + ow.AppendDataWithLabel("rules_count", fmt.Sprintf("%d", firewall.RulesCount), "Rules Count") + + switch common.OutputFormat { + case "json": + ow.ToJSON(firewall, common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Println("VPC Firewall Details:") + fmt.Printf("ID: %s\n", firewall.ID) + fmt.Printf("Name: %s\n", firewall.Name) + fmt.Printf("Network ID: %s\n", firewall.NetworkID) + fmt.Printf("Rules Count: %d\n", firewall.RulesCount) + fmt.Printf("Instance Count: %d\n", firewall.InstanceCount) + fmt.Printf("Cluster Count: %d\n", firewall.ClusterCount) + fmt.Printf("Load Balancer Count: %d\n", firewall.LoadBalancerCount) + + if len(rules) > 0 { + fmt.Println("\nRules:") + for _, rule := range rules { + fmt.Printf(" - %s: %s %s ports %s-%s %s (%s)\n", + rule.ID, rule.Direction, rule.Protocol, + rule.StartPort, rule.EndPort, + strings.Join(rule.Cidr, ","), rule.Action) + } + } + } + }, +} diff --git a/cmd/vpc/vpc_firewall_update.go b/cmd/vpc/vpc_firewall_update.go new file mode 100644 index 00000000..b12080d5 --- /dev/null +++ b/cmd/vpc/vpc_firewall_update.go @@ -0,0 +1,58 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcFirewallUpdateCmd = &cobra.Command{ + Use: "update", + Aliases: []string{"rename", "change"}, + Short: "Update a VPC firewall", + Example: "civo vpc firewall update OLD_NAME NEW_NAME", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + firewall, err := client.FindVPCFirewall(args[0]) + if err != nil { + utility.Error("Firewall %s", err) + os.Exit(1) + } + + _, err = client.RenameVPCFirewall(firewall.ID, &civogo.FirewallConfig{ + Name: args[1], + NetworkID: firewall.NetworkID, + }) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": firewall.ID, "name": firewall.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("The VPC firewall called %s with ID %s was renamed to %s\n", utility.Green(firewall.Name), utility.Green(firewall.ID), utility.Green(args[1])) + } + }, +} diff --git a/cmd/vpc/vpc_ip.go b/cmd/vpc/vpc_ip.go new file mode 100644 index 00000000..94878915 --- /dev/null +++ b/cmd/vpc/vpc_ip.go @@ -0,0 +1,20 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var vpcIPCmd = &cobra.Command{ + Use: "ip", + Aliases: []string{"ips"}, + Short: "Details of Civo VPC reserved IPs", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} diff --git a/cmd/vpc/vpc_ip_assign.go b/cmd/vpc/vpc_ip_assign.go new file mode 100644 index 00000000..9a379075 --- /dev/null +++ b/cmd/vpc/vpc_ip_assign.go @@ -0,0 +1,68 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var ( + vpcIPAssignResourceID string + vpcIPAssignResourceType string +) + +var vpcIPAssignCmd = &cobra.Command{ + Use: "assign", + Aliases: []string{"attach"}, + Example: `civo vpc ip assign IP_NAME --resource-id RESOURCE_ID --resource-type instance`, + Short: "Assign a VPC IP to a resource", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + ip, err := client.FindVPCIP(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC IP in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC IP with that value in your account") + os.Exit(1) + } + utility.Error("%s", err) + os.Exit(1) + } + + _, err = client.AssignVPCIP(ip.ID, vpcIPAssignResourceID, vpcIPAssignResourceType, client.Region) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Assigned VPC IP %s to %s %s\n", utility.Green(ip.Name), vpcIPAssignResourceType, utility.Green(vpcIPAssignResourceID)) + } + }, +} diff --git a/cmd/vpc/vpc_ip_create.go b/cmd/vpc/vpc_ip_create.go new file mode 100644 index 00000000..2a6bc4ce --- /dev/null +++ b/cmd/vpc/vpc_ip_create.go @@ -0,0 +1,62 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcIPName string + +var vpcIPCreateCmd = &cobra.Command{ + Use: "reserve", + Aliases: []string{"new", "add", "allocate", "create"}, + Example: `civo vpc ip reserve +civo vpc ip reserve --name "my-ip"`, + Short: "Reserve a new VPC IP", + Args: cobra.MinimumNArgs(0), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + createReq := &civogo.CreateIPRequest{ + Region: client.Region, + } + if vpcIPName != "" { + createReq.Name = vpcIPName + } + + ip, err := client.NewVPCIP(createReq) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + if vpcIPName != "" { + fmt.Printf("Reserved VPC IP called %s with ID %s\n", utility.Green(vpcIPName), utility.Green(ip.ID)) + } else { + fmt.Printf("Reserved VPC IP with ID %s\n", utility.Green(ip.ID)) + } + } + }, +} diff --git a/cmd/vpc/vpc_ip_list.go b/cmd/vpc/vpc_ip_list.go new file mode 100644 index 00000000..05cf45c4 --- /dev/null +++ b/cmd/vpc/vpc_ip_list.go @@ -0,0 +1,59 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcIPListCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list", "all"}, + Example: "civo vpc ip ls", + Short: "List VPC reserved IPs", + Long: `List all available VPC reserved IPs. +If you wish to use a custom format, the available fields are: + + * id + * name + * address + * assigned_to`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + ips, err := client.ListVPCIPs() + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + + for _, ip := range ips.Items { + ow.StartLine() + ow.AppendDataWithLabel("id", ip.ID, "ID") + ow.AppendDataWithLabel("name", ip.Name, "Name") + ow.AppendDataWithLabel("address", ip.IP, "Address") + if ip.AssignedTo.ID != "" { + ow.AppendDataWithLabel("assigned_to", fmt.Sprintf("%s (%s)", ip.AssignedTo.Name, ip.AssignedTo.Type), "Assigned To(type)") + } else { + ow.AppendDataWithLabel("assigned_to", "No resource", "Assigned To(type)") + } + } + + ow.FinishAndPrintOutput() + }, +} diff --git a/cmd/vpc/vpc_ip_remove.go b/cmd/vpc/vpc_ip_remove.go new file mode 100644 index 00000000..11d84e74 --- /dev/null +++ b/cmd/vpc/vpc_ip_remove.go @@ -0,0 +1,68 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcIPRemoveCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"unallocate", "free", "remove", "rm"}, + Example: `civo vpc ip delete IP_NAME`, + Short: "Delete a VPC reserved IP", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + ip, err := client.FindVPCIP(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC IP in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC IP with that value in your account") + os.Exit(1) + } + utility.Error("%s", err) + os.Exit(1) + } + + if utility.UserConfirmedDeletion("VPC IP", common.DefaultYes, ip.Name) { + _, err = client.DeleteVPCIP(ip.ID) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": ip.ID, "name": ip.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("VPC IP called %s with ID %s was deleted\n", utility.Green(ip.Name), utility.Green(ip.ID)) + } + } else { + fmt.Println("Operation aborted.") + } + }, +} diff --git a/cmd/vpc/vpc_ip_show.go b/cmd/vpc/vpc_ip_show.go new file mode 100644 index 00000000..ac8812f4 --- /dev/null +++ b/cmd/vpc/vpc_ip_show.go @@ -0,0 +1,70 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcIPShowCmd = &cobra.Command{ + Use: "show [IP-NAME/IP-ID/IP-ADDRESS]", + Short: "Show details of a specific VPC reserved IP", + Aliases: []string{"get", "describe", "inspect"}, + Args: cobra.ExactArgs(1), + Example: "civo vpc ip show IP_NAME", + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + ip, err := client.FindVPCIP(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC IP in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC IP with that value in your account") + os.Exit(1) + } + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + ow.StartLine() + ow.AppendDataWithLabel("id", ip.ID, "ID") + ow.AppendDataWithLabel("name", ip.Name, "Name") + ow.AppendDataWithLabel("address", ip.IP, "Address") + + switch common.OutputFormat { + case "json": + ow.ToJSON(ip, common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Println("VPC IP Details:") + fmt.Printf("ID: %s\n", ip.ID) + fmt.Printf("Name: %s\n", ip.Name) + fmt.Printf("IP: %s\n", ip.IP) + if ip.AssignedTo.ID != "" { + fmt.Printf("Assigned To: %s (%s)\n", ip.AssignedTo.Name, ip.AssignedTo.Type) + } else { + fmt.Println("Assigned To: No resource") + } + } + }, +} diff --git a/cmd/vpc/vpc_ip_unassign.go b/cmd/vpc/vpc_ip_unassign.go new file mode 100644 index 00000000..5ddfe82a --- /dev/null +++ b/cmd/vpc/vpc_ip_unassign.go @@ -0,0 +1,67 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcIPUnassignCmd = &cobra.Command{ + Use: "unassign", + Aliases: []string{"detach"}, + Example: `civo vpc ip unassign IP_NAME`, + Short: "Unassign a VPC IP from a resource", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + ip, err := client.FindVPCIP(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC IP in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC IP with that value in your account") + os.Exit(1) + } + utility.Error("%s", err) + os.Exit(1) + } + + if utility.UserConfirmedUnassign("VPC IP", common.DefaultYes, ip.Name) { + _, err = client.UnassignVPCIP(ip.ID, client.Region) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Unassigned VPC IP %s from Civo resource\n", utility.Green(ip.Name)) + } + } else { + fmt.Println("Aborted") + } + }, +} diff --git a/cmd/vpc/vpc_ip_update.go b/cmd/vpc/vpc_ip_update.go new file mode 100644 index 00000000..1addb6b2 --- /dev/null +++ b/cmd/vpc/vpc_ip_update.go @@ -0,0 +1,68 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcIPUpdateName string + +var vpcIPUpdateCmd = &cobra.Command{ + Use: "update", + Aliases: []string{"rename", "change"}, + Example: `civo vpc ip update IP_NAME/IP_ID --name NEW_NAME`, + Short: "Update a VPC reserved IP", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + ip, err := client.FindVPCIP(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC IP in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC IP with that value in your account") + os.Exit(1) + } + utility.Error("%s", err) + os.Exit(1) + } + + ip, err = client.UpdateVPCIP(ip.ID, &civogo.UpdateIPRequest{ + Name: vpcIPUpdateName, + Region: client.Region, + }) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Renamed VPC IP to %s with ID %s\n", utility.Green(vpcIPUpdateName), utility.Green(ip.ID)) + } + }, +} diff --git a/cmd/vpc/vpc_loadbalancer.go b/cmd/vpc/vpc_loadbalancer.go new file mode 100644 index 00000000..bfcd7605 --- /dev/null +++ b/cmd/vpc/vpc_loadbalancer.go @@ -0,0 +1,20 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var vpcLoadBalancerCmd = &cobra.Command{ + Use: "loadbalancer", + Aliases: []string{"loadbalancers", "lb"}, + Short: "Details of Civo VPC load balancers", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} diff --git a/cmd/vpc/vpc_loadbalancer_create.go b/cmd/vpc/vpc_loadbalancer_create.go new file mode 100644 index 00000000..d52e23da --- /dev/null +++ b/cmd/vpc/vpc_loadbalancer_create.go @@ -0,0 +1,139 @@ +package vpc + +import ( + "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 ( + vpcLBNetwork string + vpcLBAlgorithm string + vpcLBExternalTrafficPolicy string + vpcLBSessionAffinity string + vpcLBSessionAffinityTimeout int32 + vpcLBEnableProxyProtocol string + vpcLBFirewallID string + vpcLBMaxConcurrentRequests int + vpcLBBackends []string +) + +var vpcLoadBalancerCreateCmd = &cobra.Command{ + Use: "create", + Aliases: []string{"new", "add"}, + Example: `civo vpc loadbalancer create NAME --network NETWORK [flags] +civo vpc loadbalancer create my-lb --network my-network --backend "10.0.0.1:80:8080:TCP:8080"`, + Short: "Create a new VPC load balancer", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(vpcLBNetwork) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + lbConfig := &civogo.LoadBalancerConfig{ + Region: client.Region, + Name: args[0], + NetworkID: network.ID, + Algorithm: vpcLBAlgorithm, + ExternalTrafficPolicy: vpcLBExternalTrafficPolicy, + SessionAffinity: vpcLBSessionAffinity, + EnableProxyProtocol: vpcLBEnableProxyProtocol, + FirewallID: vpcLBFirewallID, + } + + if cmd.Flags().Changed("session-affinity-timeout") { + lbConfig.SessionAffinityConfigTimeout = vpcLBSessionAffinityTimeout + } + + if cmd.Flags().Changed("max-concurrent-requests") { + lbConfig.MaxConcurrentRequests = &vpcLBMaxConcurrentRequests + } + + if len(vpcLBBackends) > 0 { + backends, err := parseBackends(vpcLBBackends) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + lbConfig.Backends = backends + } + + lb, err := client.CreateVPCLoadBalancer(lbConfig) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": lb.ID, "name": lb.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Created a VPC load balancer called %s with ID %s\n", utility.Green(lb.Name), utility.Green(lb.ID)) + } + }, +} + +// parseBackends parses backend strings in format "ip:source_port:target_port:protocol:health_check_port" +func parseBackends(backends []string) ([]civogo.LoadBalancerBackendConfig, error) { + var result []civogo.LoadBalancerBackendConfig + for _, b := range backends { + parts := strings.Split(b, ":") + if len(parts) < 3 { + return nil, fmt.Errorf("invalid backend format '%s', expected ip:source_port:target_port[:protocol[:health_check_port]]", b) + } + + sourcePort, err := strconv.ParseInt(parts[1], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid source port '%s': %s", parts[1], err) + } + + targetPort, err := strconv.ParseInt(parts[2], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid target port '%s': %s", parts[2], err) + } + + backend := civogo.LoadBalancerBackendConfig{ + IP: parts[0], + SourcePort: int32(sourcePort), + TargetPort: int32(targetPort), + } + + if len(parts) > 3 { + backend.Protocol = parts[3] + } + if len(parts) > 4 { + healthPort, err := strconv.ParseInt(parts[4], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid health check port '%s': %s", parts[4], err) + } + backend.HealthCheckPort = int32(healthPort) + } + + result = append(result, backend) + } + return result, nil +} diff --git a/cmd/vpc/vpc_loadbalancer_list.go b/cmd/vpc/vpc_loadbalancer_list.go new file mode 100644 index 00000000..67402509 --- /dev/null +++ b/cmd/vpc/vpc_loadbalancer_list.go @@ -0,0 +1,74 @@ +package vpc + +import ( + "fmt" + "os" + "strings" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcLoadBalancerListCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list", "all"}, + Example: `civo vpc loadbalancer ls -o custom -f "ID: Name"`, + Short: "List VPC load balancers", + Long: `List all VPC load balancers. +If you wish to use a custom format, the available fields are: + + * id + * name + * algorithm + * public_ip + * private_ip + * state + * backends`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + lbs, err := client.ListVPCLoadBalancers() + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + for _, lb := range lbs { + ow.StartLine() + ow.AppendDataWithLabel("id", lb.ID, "ID") + ow.AppendDataWithLabel("name", lb.Name, "Name") + ow.AppendDataWithLabel("algorithm", lb.Algorithm, "Algorithm") + ow.AppendDataWithLabel("public_ip", lb.PublicIP, "Public IP") + ow.AppendDataWithLabel("private_ip", lb.PrivateIP, "Private IP") + ow.AppendDataWithLabel("state", lb.State, "State") + + var backendList []string + for _, backend := range lb.Backends { + backendList = append(backendList, backend.IP) + } + ow.AppendDataWithLabel("backends", fmt.Sprintf("%d", len(lb.Backends)), "Backends") + + if common.OutputFormat == "json" || common.OutputFormat == "custom" { + ow.AppendDataWithLabel("firewall_id", lb.FirewallID, "Firewall ID") + ow.AppendDataWithLabel("cluster_id", lb.ClusterID, "Cluster ID") + ow.AppendDataWithLabel("external_traffic_policy", lb.ExternalTrafficPolicy, "External Traffic Policy") + ow.AppendDataWithLabel("session_affinity", lb.SessionAffinity, "Session Affinity") + ow.AppendData("Backend IPs", strings.Join(backendList, ", ")) + } + } + + ow.FinishAndPrintOutput() + }, +} diff --git a/cmd/vpc/vpc_loadbalancer_remove.go b/cmd/vpc/vpc_loadbalancer_remove.go new file mode 100644 index 00000000..0abad703 --- /dev/null +++ b/cmd/vpc/vpc_loadbalancer_remove.go @@ -0,0 +1,99 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/pkg/pluralize" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcLBResourceList []utility.Resource +var vpcLoadBalancerRemoveCmd = &cobra.Command{ + Use: "remove [NAME]", + Aliases: []string{"rm", "delete", "destroy"}, + Example: "civo vpc loadbalancer remove NAME", + Short: "Remove a VPC load balancer", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + if len(args) == 1 { + lb, err := client.FindVPCLoadBalancer(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC load balancer in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC load balancer with that name in your account") + os.Exit(1) + } + } + vpcLBResourceList = append(vpcLBResourceList, utility.Resource{ID: lb.ID, Name: lb.Name}) + } else { + for _, v := range args { + lb, err := client.FindVPCLoadBalancer(v) + if err == nil { + vpcLBResourceList = append(vpcLBResourceList, utility.Resource{ID: lb.ID, Name: lb.Name}) + } + } + } + + lbNameList := []string{} + for _, v := range vpcLBResourceList { + lbNameList = append(lbNameList, v.Name) + } + + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(vpcLBResourceList), "VPC load balancer"), common.DefaultYes, strings.Join(lbNameList, ", ")) { + for _, v := range vpcLBResourceList { + _, err = client.DeleteVPCLoadBalancer(v.ID) + if err != nil { + utility.Error("error deleting the VPC load balancer: %s", err) + os.Exit(1) + } + } + + ow := utility.NewOutputWriter() + for _, v := range vpcLBResourceList { + ow.StartLine() + ow.AppendDataWithLabel("id", v.ID, "ID") + ow.AppendDataWithLabel("name", v.Name, "Name") + } + + switch common.OutputFormat { + case "json": + if len(vpcLBResourceList) == 1 { + ow.WriteSingleObjectJSON(common.PrettySet) + } else { + ow.WriteMultipleObjectsJSON(common.PrettySet) + } + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("The %s (%s) %s been deleted\n", + pluralize.Pluralize(len(vpcLBResourceList), "VPC load balancer"), + utility.Green(strings.Join(lbNameList, ", ")), + pluralize.Has(len(vpcLBResourceList)), + ) + } + } else { + fmt.Println("Operation aborted.") + } + }, +} diff --git a/cmd/vpc/vpc_loadbalancer_show.go b/cmd/vpc/vpc_loadbalancer_show.go new file mode 100644 index 00000000..91f3ab0e --- /dev/null +++ b/cmd/vpc/vpc_loadbalancer_show.go @@ -0,0 +1,97 @@ +package vpc + +import ( + "fmt" + "os" + "strings" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcLoadBalancerShowCmd = &cobra.Command{ + Use: "show", + Aliases: []string{"get", "inspect"}, + Example: "civo vpc loadbalancer show ID/NAME", + Args: cobra.MinimumNArgs(1), + Short: "Show VPC load balancer", + Long: `Show a specified VPC load balancer. +If you wish to use a custom format, the available fields are: + + * id + * name + * algorithm + * public_ip + * state + * private_ip + * firewall_id + * cluster_id + * external_traffic_policy + * session_affinity`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + lb, err := client.FindVPCLoadBalancer(args[0]) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + ow.StartLine() + ow.AppendDataWithLabel("id", lb.ID, "ID") + ow.AppendDataWithLabel("name", lb.Name, "Name") + ow.AppendDataWithLabel("algorithm", lb.Algorithm, "Algorithm") + ow.AppendDataWithLabel("public_ip", lb.PublicIP, "Public IP") + ow.AppendDataWithLabel("state", lb.State, "State") + + if common.OutputFormat == "json" || common.OutputFormat == "custom" { + ow.AppendDataWithLabel("private_ip", lb.PrivateIP, "Private IP") + ow.AppendDataWithLabel("firewall_id", lb.FirewallID, "Firewall ID") + ow.AppendDataWithLabel("cluster_id", lb.ClusterID, "Cluster ID") + ow.AppendDataWithLabel("external_traffic_policy", lb.ExternalTrafficPolicy, "External Traffic Policy") + ow.AppendDataWithLabel("session_affinity", lb.SessionAffinity, "Session Affinity") + } + + var backendList []string + for _, backend := range lb.Backends { + backendList = append(backendList, backend.IP) + } + ow.AppendData("Backends", strings.Join(backendList, ", ")) + + switch common.OutputFormat { + case "json": + ow.ToJSON(lb, common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Println("VPC Load Balancer Details:") + fmt.Printf("ID: %s\n", lb.ID) + fmt.Printf("Name: %s\n", lb.Name) + fmt.Printf("Algorithm: %s\n", lb.Algorithm) + fmt.Printf("Public IP: %s\n", lb.PublicIP) + fmt.Printf("Private IP: %s\n", lb.PrivateIP) + fmt.Printf("State: %s\n", lb.State) + fmt.Printf("Firewall ID: %s\n", lb.FirewallID) + fmt.Printf("External Traffic Policy: %s\n", lb.ExternalTrafficPolicy) + fmt.Printf("Session Affinity: %s\n", lb.SessionAffinity) + if len(lb.Backends) > 0 { + fmt.Println("\nBackends:") + for _, backend := range lb.Backends { + fmt.Printf(" - %s (port %d -> %d, %s)\n", backend.IP, backend.SourcePort, backend.TargetPort, backend.Protocol) + } + } + } + }, +} diff --git a/cmd/vpc/vpc_loadbalancer_update.go b/cmd/vpc/vpc_loadbalancer_update.go new file mode 100644 index 00000000..2ed91513 --- /dev/null +++ b/cmd/vpc/vpc_loadbalancer_update.go @@ -0,0 +1,104 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var ( + vpcLBUpdateName string + vpcLBUpdateAlgorithm string + vpcLBUpdateExternalTrafficPolicy string + vpcLBUpdateSessionAffinity string + vpcLBUpdateSessionAffinityTimeout int32 + vpcLBUpdateEnableProxyProtocol string + vpcLBUpdateFirewallID string + vpcLBUpdateMaxConcurrentRequests int + vpcLBUpdateBackends []string +) + +var vpcLoadBalancerUpdateCmd = &cobra.Command{ + Use: "update", + Aliases: []string{"change", "modify"}, + Example: "civo vpc loadbalancer update LB_NAME/LB_ID [flags]", + Short: "Update a VPC load balancer", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + lb, err := client.FindVPCLoadBalancer(args[0]) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + updateConfig := &civogo.LoadBalancerUpdateConfig{ + Region: client.Region, + } + + if cmd.Flags().Changed("name") { + updateConfig.Name = vpcLBUpdateName + } + if cmd.Flags().Changed("algorithm") { + updateConfig.Algorithm = vpcLBUpdateAlgorithm + } + if cmd.Flags().Changed("external-traffic-policy") { + updateConfig.ExternalTrafficPolicy = vpcLBUpdateExternalTrafficPolicy + } + if cmd.Flags().Changed("session-affinity") { + updateConfig.SessionAffinity = vpcLBUpdateSessionAffinity + } + if cmd.Flags().Changed("session-affinity-timeout") { + updateConfig.SessionAffinityConfigTimeout = vpcLBUpdateSessionAffinityTimeout + } + if cmd.Flags().Changed("enable-proxy-protocol") { + updateConfig.EnableProxyProtocol = vpcLBUpdateEnableProxyProtocol + } + if cmd.Flags().Changed("firewall-id") { + updateConfig.FirewallID = vpcLBUpdateFirewallID + } + if cmd.Flags().Changed("max-concurrent-requests") { + updateConfig.MaxConcurrentRequests = &vpcLBUpdateMaxConcurrentRequests + } + if len(vpcLBUpdateBackends) > 0 { + backends, err := parseBackends(vpcLBUpdateBackends) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + updateConfig.Backends = backends + } + + updatedLB, err := client.UpdateVPCLoadBalancer(lb.ID, updateConfig) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": updatedLB.ID, "name": updatedLB.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Updated VPC load balancer %s with ID %s\n", utility.Green(updatedLB.Name), utility.Green(updatedLB.ID)) + } + }, +} diff --git a/cmd/vpc/vpc_network.go b/cmd/vpc/vpc_network.go new file mode 100644 index 00000000..539714e5 --- /dev/null +++ b/cmd/vpc/vpc_network.go @@ -0,0 +1,20 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var vpcNetworkCmd = &cobra.Command{ + Use: "network", + Aliases: []string{"networks", "net"}, + Short: "Details of Civo VPC networks", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} diff --git a/cmd/vpc/vpc_network_create.go b/cmd/vpc/vpc_network_create.go new file mode 100644 index 00000000..7eebef04 --- /dev/null +++ b/cmd/vpc/vpc_network_create.go @@ -0,0 +1,78 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var ( + vpcNetCIDRv4 string + vpcNetNameserversV4 []string + vpcNetIPv4Enabled bool + vpcNetIPv6Enabled bool + vpcNetNameserversV6 []string +) + +var vpcNetworkCreateCmd = &cobra.Command{ + Use: "create", + Aliases: []string{"new", "add"}, + Example: "civo vpc network create NAME [flags]", + Short: "Create a new VPC network", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + networkConfig := civogo.NetworkConfig{ + Label: args[0], + Region: client.Region, + } + + if vpcNetCIDRv4 != "" { + networkConfig.CIDRv4 = vpcNetCIDRv4 + } + if len(vpcNetNameserversV4) > 0 { + networkConfig.NameserversV4 = vpcNetNameserversV4 + } + if cmd.Flags().Changed("ipv4-enabled") { + networkConfig.IPv4Enabled = &vpcNetIPv4Enabled + } + if cmd.Flags().Changed("ipv6-enabled") { + networkConfig.IPv6Enabled = &vpcNetIPv6Enabled + } + if len(vpcNetNameserversV6) > 0 { + networkConfig.NameserversV6 = vpcNetNameserversV6 + } + + network, err := client.CreateVPCNetwork(networkConfig) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": network.ID, "label": network.Label}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Created a VPC network called %s with ID %s\n", utility.Green(network.Label), utility.Green(network.ID)) + } + }, +} diff --git a/cmd/vpc/vpc_network_list.go b/cmd/vpc/vpc_network_list.go new file mode 100644 index 00000000..5cf49b9e --- /dev/null +++ b/cmd/vpc/vpc_network_list.go @@ -0,0 +1,63 @@ +package vpc + +import ( + "os" + "strconv" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcNetworkListCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list", "all"}, + Example: `civo vpc network ls -o custom -f "ID: Label"`, + Short: "List VPC networks", + Long: `List all available VPC networks. +If you wish to use a custom format, the available fields are: + + * id + * label + * default + * cidr + * status + * ipv4_enabled + * ipv6_enabled + * free_ip_count`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + networks, err := client.ListVPCNetworks() + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + + for _, network := range networks { + ow.StartLine() + ow.AppendDataWithLabel("id", network.ID, "ID") + ow.AppendDataWithLabel("label", network.Label, "Label") + ow.AppendDataWithLabel("default", strconv.FormatBool(network.Default), "Default") + ow.AppendDataWithLabel("cidr", network.CIDR, "CIDR") + ow.AppendDataWithLabel("status", network.Status, "Status") + ow.AppendDataWithLabel("ipv4_enabled", strconv.FormatBool(network.IPv4Enabled), "IPv4 Enabled") + ow.AppendDataWithLabel("ipv6_enabled", strconv.FormatBool(network.IPv6Enabled), "IPv6 Enabled") + ow.AppendDataWithLabel("free_ip_count", strconv.Itoa(network.FreeIPCount), "Free IP Count") + } + + ow.FinishAndPrintOutput() + }, +} diff --git a/cmd/vpc/vpc_network_remove.go b/cmd/vpc/vpc_network_remove.go new file mode 100644 index 00000000..4970ed29 --- /dev/null +++ b/cmd/vpc/vpc_network_remove.go @@ -0,0 +1,109 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/pkg/pluralize" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcNetworkResourceList []utility.Resource +var vpcNetworkRemoveCmd = &cobra.Command{ + Use: "remove", + Aliases: []string{"rm", "delete", "destroy"}, + Example: "civo vpc network remove NAME", + Short: "Remove a VPC network", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + if len(args) == 1 { + network, err := client.FindVPCNetwork(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC network in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC network with that name in your account") + os.Exit(1) + } + } + vpcNetworkResourceList = append(vpcNetworkResourceList, utility.Resource{ID: network.ID, Name: network.Label}) + } else { + for _, v := range args { + network, err := client.FindVPCNetwork(v) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC network in your account", utility.Red(v)) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC network with that name in your account") + os.Exit(1) + } + } + if err == nil { + vpcNetworkResourceList = append(vpcNetworkResourceList, utility.Resource{ID: network.ID, Name: network.Label}) + } + } + } + + networkNameList := []string{} + for _, v := range vpcNetworkResourceList { + networkNameList = append(networkNameList, v.Name) + } + + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(vpcNetworkResourceList), "VPC network"), common.DefaultYes, strings.Join(networkNameList, ", ")) { + for _, v := range vpcNetworkResourceList { + _, err = client.DeleteVPCNetwork(v.ID) + if err != nil { + utility.Error("Error deleting the VPC network: %s", err) + os.Exit(1) + } + } + + ow := utility.NewOutputWriter() + for _, v := range vpcNetworkResourceList { + ow.StartLine() + ow.AppendDataWithLabel("id", v.ID, "ID") + ow.AppendDataWithLabel("label", v.Name, "Name") + } + + switch common.OutputFormat { + case "json": + if len(vpcNetworkResourceList) == 1 { + ow.WriteSingleObjectJSON(common.PrettySet) + } else { + ow.WriteMultipleObjectsJSON(common.PrettySet) + } + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("The %s (%s) %s been deleted\n", + pluralize.Pluralize(len(vpcNetworkResourceList), "VPC network"), + utility.Green(strings.Join(networkNameList, ", ")), + pluralize.Has(len(vpcNetworkResourceList)), + ) + } + } else { + fmt.Println("Operation aborted.") + } + }, +} diff --git a/cmd/vpc/vpc_network_show.go b/cmd/vpc/vpc_network_show.go new file mode 100644 index 00000000..72c3de23 --- /dev/null +++ b/cmd/vpc/vpc_network_show.go @@ -0,0 +1,76 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcNetworkShowCmd = &cobra.Command{ + Use: "show [NETWORK-NAME/NETWORK-ID]", + Short: "Show details of a specific VPC network", + Aliases: []string{"get", "describe", "inspect"}, + Args: cobra.ExactArgs(1), + Example: "civo vpc network show NETWORK_NAME", + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(args[0]) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + network, err = client.GetVPCNetwork(network.ID) + if err != nil { + utility.Error("Failed to retrieve network: %s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + ow.StartLine() + ow.AppendDataWithLabel("id", network.ID, "ID") + ow.AppendDataWithLabel("label", network.Label, "Label") + ow.AppendDataWithLabel("default", utility.BoolToYesNo(network.Default), "Default") + ow.AppendDataWithLabel("cidr", network.CIDR, "CIDR") + ow.AppendDataWithLabel("status", network.Status, "Status") + ow.AppendDataWithLabel("ipv4_enabled", utility.BoolToYesNo(network.IPv4Enabled), "IPv4 Enabled") + ow.AppendDataWithLabel("ipv6_enabled", utility.BoolToYesNo(network.IPv6Enabled), "IPv6 Enabled") + + switch common.OutputFormat { + case "json": + ow.ToJSON(network, common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Println("VPC Network Details:") + fmt.Printf("ID: %s\n", network.ID) + fmt.Printf("Name: %s\n", network.Label) + fmt.Printf("Default: %s\n", utility.BoolToYesNo(network.Default)) + fmt.Printf("CIDR: %s\n", network.CIDR) + fmt.Printf("Status: %s\n", network.Status) + fmt.Printf("IPv4 Enabled: %s\n", utility.BoolToYesNo(network.IPv4Enabled)) + fmt.Printf("IPv6 Enabled: %s\n", utility.BoolToYesNo(network.IPv6Enabled)) + + if len(network.NameserversV4) > 0 { + fmt.Printf("Nameservers IPv4: %s\n", utility.SliceToString(network.NameserversV4)) + } + if len(network.NameserversV6) > 0 { + fmt.Printf("Nameservers IPv6: %s\n", utility.SliceToString(network.NameserversV6)) + } + } + }, +} diff --git a/cmd/vpc/vpc_network_update.go b/cmd/vpc/vpc_network_update.go new file mode 100644 index 00000000..3b64e09b --- /dev/null +++ b/cmd/vpc/vpc_network_update.go @@ -0,0 +1,54 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var vpcNetworkUpdateCmd = &cobra.Command{ + Use: "update", + Aliases: []string{"rename", "change", "modify"}, + Example: "civo vpc network update OLD_NAME NEW_NAME", + Short: "Rename a VPC network", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + oldNetwork, err := client.FindVPCNetwork(args[0]) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + network, err := client.RenameVPCNetwork(args[1], oldNetwork.ID) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": network.ID, "label": network.Label}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Renamed the VPC network called %s with ID %s to %s\n", utility.Green(oldNetwork.Label), utility.Green(network.ID), utility.Green(network.Label)) + } + }, +} diff --git a/cmd/vpc/vpc_subnet.go b/cmd/vpc/vpc_subnet.go new file mode 100644 index 00000000..ba63265b --- /dev/null +++ b/cmd/vpc/vpc_subnet.go @@ -0,0 +1,20 @@ +package vpc + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var vpcSubnetCmd = &cobra.Command{ + Use: "subnet", + Aliases: []string{"subnets"}, + Short: "Details of Civo VPC subnets", + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Help() + if err != nil { + return err + } + return errors.New("a valid subcommand is required") + }, +} diff --git a/cmd/vpc/vpc_subnet_attach.go b/cmd/vpc/vpc_subnet_attach.go new file mode 100644 index 00000000..3bcd12af --- /dev/null +++ b/cmd/vpc/vpc_subnet_attach.go @@ -0,0 +1,70 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var ( + subnetAttachNetworkID string + subnetAttachResourceID string + subnetAttachResourceType string +) + +var vpcSubnetAttachCmd = &cobra.Command{ + Use: "attach [SUBNET-NAME/SUBNET-ID]", + Aliases: []string{"connect"}, + Example: "civo vpc subnet attach SUBNET_NAME --network NETWORK_NAME --resource-id RESOURCE_ID --resource-type instance", + Short: "Attach a VPC subnet to an instance", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(subnetAttachNetworkID) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + subnet, err := client.FindVPCSubnet(args[0], network.ID) + if err != nil { + utility.Error("Subnet %s", err) + os.Exit(1) + } + + route, err := client.AttachVPCSubnetToInstance(network.ID, subnet.ID, &civogo.CreateRoute{ + ResourceID: subnetAttachResourceID, + ResourceType: subnetAttachResourceType, + }) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": route.ID, "subnet_id": subnet.ID, "resource_id": route.ResourceID}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Attached VPC subnet %s to resource %s\n", utility.Green(subnet.Name), utility.Green(subnetAttachResourceID)) + } + }, +} diff --git a/cmd/vpc/vpc_subnet_create.go b/cmd/vpc/vpc_subnet_create.go new file mode 100644 index 00000000..aed86cc8 --- /dev/null +++ b/cmd/vpc/vpc_subnet_create.go @@ -0,0 +1,59 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var subnetCreateNetworkID string + +var vpcSubnetCreateCmd = &cobra.Command{ + Use: "create", + Aliases: []string{"new", "add"}, + Example: "civo vpc subnet create SUBNET_NAME --network NETWORK_NAME", + Short: "Create a new VPC subnet", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(subnetCreateNetworkID) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + subnet, err := client.CreateVPCSubnet(network.ID, civogo.SubnetConfig{ + Name: args[0], + }) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriterWithMap(map[string]string{"id": subnet.ID, "name": subnet.Name}) + + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Created a VPC subnet called %s with ID %s\n", utility.Green(subnet.Name), utility.Green(subnet.ID)) + } + }, +} diff --git a/cmd/vpc/vpc_subnet_detach.go b/cmd/vpc/vpc_subnet_detach.go new file mode 100644 index 00000000..0d9f4f29 --- /dev/null +++ b/cmd/vpc/vpc_subnet_detach.go @@ -0,0 +1,61 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var subnetDetachNetworkID string + +var vpcSubnetDetachCmd = &cobra.Command{ + Use: "detach [SUBNET-NAME/SUBNET-ID]", + Aliases: []string{"disconnect"}, + Example: "civo vpc subnet detach SUBNET_NAME --network NETWORK_NAME", + Short: "Detach a VPC subnet from an instance", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(subnetDetachNetworkID) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + subnet, err := client.FindVPCSubnet(args[0], network.ID) + if err != nil { + utility.Error("Subnet %s", err) + os.Exit(1) + } + + _, err = client.DetachVPCSubnetFromInstance(network.ID, subnet.ID) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + switch common.OutputFormat { + case "json": + ow.WriteSingleObjectJSON(common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("Detached VPC subnet %s from instance\n", utility.Green(subnet.Name)) + } + }, +} diff --git a/cmd/vpc/vpc_subnet_list.go b/cmd/vpc/vpc_subnet_list.go new file mode 100644 index 00000000..62bbf7d8 --- /dev/null +++ b/cmd/vpc/vpc_subnet_list.go @@ -0,0 +1,64 @@ +package vpc + +import ( + "os" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var subnetListNetworkID string + +var vpcSubnetListCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list", "all"}, + Example: "civo vpc subnet ls --network NETWORK_NAME", + Short: "List VPC subnets", + Long: `List all subnets in a VPC network. +If you wish to use a custom format, the available fields are: + + * id + * name + * network_id + * subnet_size + * status`, + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(subnetListNetworkID) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + subnets, err := client.ListVPCSubnets(network.ID) + if err != nil { + utility.Error("%s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + + for _, subnet := range subnets { + ow.StartLine() + ow.AppendDataWithLabel("id", subnet.ID, "ID") + ow.AppendDataWithLabel("name", subnet.Name, "Name") + ow.AppendDataWithLabel("network_id", subnet.NetworkID, "Network ID") + ow.AppendDataWithLabel("subnet_size", subnet.SubnetSize, "Subnet Size") + ow.AppendDataWithLabel("status", subnet.Status, "Status") + } + + ow.FinishAndPrintOutput() + }, +} diff --git a/cmd/vpc/vpc_subnet_remove.go b/cmd/vpc/vpc_subnet_remove.go new file mode 100644 index 00000000..e379c0b7 --- /dev/null +++ b/cmd/vpc/vpc_subnet_remove.go @@ -0,0 +1,107 @@ +package vpc + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/civo/civogo" + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/pkg/pluralize" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var subnetRemoveNetworkID string +var vpcSubnetResourceList []utility.Resource + +var vpcSubnetRemoveCmd = &cobra.Command{ + Use: "remove", + Aliases: []string{"rm", "delete", "destroy"}, + Example: "civo vpc subnet remove SUBNET_NAME --network NETWORK_NAME", + Short: "Remove a VPC subnet", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(subnetRemoveNetworkID) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + if len(args) == 1 { + subnet, err := client.FindVPCSubnet(args[0], network.ID) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s VPC subnet in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one VPC subnet with that name in your account") + os.Exit(1) + } + } + vpcSubnetResourceList = append(vpcSubnetResourceList, utility.Resource{ID: subnet.ID, Name: subnet.Name}) + } else { + for _, v := range args { + subnet, err := client.FindVPCSubnet(v, network.ID) + if err == nil { + vpcSubnetResourceList = append(vpcSubnetResourceList, utility.Resource{ID: subnet.ID, Name: subnet.Name}) + } + } + } + + subnetNameList := []string{} + for _, v := range vpcSubnetResourceList { + subnetNameList = append(subnetNameList, v.Name) + } + + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(vpcSubnetResourceList), "VPC subnet"), common.DefaultYes, strings.Join(subnetNameList, ", ")) { + for _, v := range vpcSubnetResourceList { + _, err = client.DeleteVPCSubnet(network.ID, v.ID) + if err != nil { + utility.Error("Error deleting the VPC subnet: %s", err) + os.Exit(1) + } + } + + ow := utility.NewOutputWriter() + for _, v := range vpcSubnetResourceList { + ow.StartLine() + ow.AppendDataWithLabel("id", v.ID, "ID") + ow.AppendDataWithLabel("name", v.Name, "Name") + } + + switch common.OutputFormat { + case "json": + if len(vpcSubnetResourceList) == 1 { + ow.WriteSingleObjectJSON(common.PrettySet) + } else { + ow.WriteMultipleObjectsJSON(common.PrettySet) + } + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Printf("The %s (%s) %s been deleted\n", + pluralize.Pluralize(len(vpcSubnetResourceList), "VPC subnet"), + utility.Green(strings.Join(subnetNameList, ", ")), + pluralize.Has(len(vpcSubnetResourceList)), + ) + } + } else { + fmt.Println("Operation aborted.") + } + }, +} diff --git a/cmd/vpc/vpc_subnet_show.go b/cmd/vpc/vpc_subnet_show.go new file mode 100644 index 00000000..4d9a0438 --- /dev/null +++ b/cmd/vpc/vpc_subnet_show.go @@ -0,0 +1,67 @@ +package vpc + +import ( + "fmt" + "os" + + "github.com/civo/cli/common" + "github.com/civo/cli/config" + "github.com/civo/cli/utility" + "github.com/spf13/cobra" +) + +var subnetShowNetworkID string + +var vpcSubnetShowCmd = &cobra.Command{ + Use: "show [SUBNET-NAME/SUBNET-ID]", + Short: "Show details of a specific VPC subnet", + Aliases: []string{"get", "describe", "inspect"}, + Args: cobra.ExactArgs(1), + Example: "civo vpc subnet show SUBNET_NAME --network NETWORK_NAME", + Run: func(cmd *cobra.Command, args []string) { + utility.EnsureCurrentRegion() + + client, err := config.CivoAPIClient() + if common.RegionSet != "" { + client.Region = common.RegionSet + } + if err != nil { + utility.Error("Creating the connection to Civo's API failed with %s", err) + os.Exit(1) + } + + network, err := client.FindVPCNetwork(subnetShowNetworkID) + if err != nil { + utility.Error("Network %s", err) + os.Exit(1) + } + + subnet, err := client.FindVPCSubnet(args[0], network.ID) + if err != nil { + utility.Error("Subnet %s", err) + os.Exit(1) + } + + ow := utility.NewOutputWriter() + ow.StartLine() + ow.AppendDataWithLabel("id", subnet.ID, "ID") + ow.AppendDataWithLabel("name", subnet.Name, "Name") + ow.AppendDataWithLabel("network_id", subnet.NetworkID, "Network ID") + ow.AppendDataWithLabel("subnet_size", subnet.SubnetSize, "Subnet Size") + ow.AppendDataWithLabel("status", subnet.Status, "Status") + + switch common.OutputFormat { + case "json": + ow.ToJSON(subnet, common.PrettySet) + case "custom": + ow.WriteCustomOutput(common.OutputFields) + default: + fmt.Println("VPC Subnet Details:") + fmt.Printf("ID: %s\n", subnet.ID) + fmt.Printf("Name: %s\n", subnet.Name) + fmt.Printf("Network ID: %s\n", subnet.NetworkID) + fmt.Printf("Subnet Size: %s\n", subnet.SubnetSize) + fmt.Printf("Status: %s\n", subnet.Status) + } + }, +} diff --git a/go.mod b/go.mod index 4633810b..101f0e7e 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -go 1.24.11 +go 1.24.13 module github.com/civo/cli @@ -6,7 +6,7 @@ require ( github.com/MichaelMure/go-term-markdown v0.1.4 github.com/adhocore/gronx v1.19.5 github.com/briandowns/spinner v1.23.2 - github.com/civo/civogo v0.6.5 + github.com/civo/civogo v0.7.0-alpha1 github.com/google/go-github/v57 v57.0.0 github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 diff --git a/go.sum b/go.sum index 1c0a0bbb..adb59041 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c h1:aprLqMn7gSPT+vd github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c/go.mod h1:Ie6SubJv/NTO9Q0UBH0QCl3Ve50lu9hjbi5YJUw03TE= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= -github.com/civo/civogo v0.6.5 h1:nS5TWJB2BnW1X26wN/nWGxYMgj6VEyZxSt/1OlKrQZw= -github.com/civo/civogo v0.6.5/go.mod h1:akFVdRAQfJi4t8pGduUOiBwaW/NSC9i45m/dzhF09AY= +github.com/civo/civogo v0.7.0-alpha1 h1:ggGS3PsOZ65C7ij9Mp7D3Oa275yX+MfHyJ63xa8Joz8= +github.com/civo/civogo v0.7.0-alpha1/go.mod h1:0RNiA3NDI1imXDADWSCtzcHjUCV02E+SnRLoZKKo1wY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= From c09c38de9f8f9bf83b986fe1b686265c1f9865d8 Mon Sep 17 00:00:00 2001 From: Gianluca Cannata Date: Tue, 3 Mar 2026 15:14:46 +0100 Subject: [PATCH 2/3] chore(github): Update github workflow go version --- .github/workflows/go.yml | 6 +++--- .github/workflows/goreleaser-check.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 43fecb03..c1322565 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,7 +15,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: ${{ vars.GO_VERSION || '1.24.11' }} + go-version: ${{ vars.GO_VERSION || '1.24.13' }} cache: true - name: Verify dependencies @@ -52,7 +52,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: ${{ vars.GO_VERSION || '1.24.11' }} + go-version: ${{ vars.GO_VERSION || '1.24.13' }} cache: true - name: Run golangci-lint @@ -71,5 +71,5 @@ jobs: - name: Run govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: ${{ vars.GO_VERSION || '1.24.11' }} + go-version-input: ${{ vars.GO_VERSION || '1.24.13' }} go-package: ./... diff --git a/.github/workflows/goreleaser-check.yml b/.github/workflows/goreleaser-check.yml index 9622123f..9cdc8fd6 100644 --- a/.github/workflows/goreleaser-check.yml +++ b/.github/workflows/goreleaser-check.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: ${{ vars.GO_VERSION || '1.24.11' }} + go-version: ${{ vars.GO_VERSION || '1.24.13' }} cache: true - name: Run GoReleaser Check From 50d77c6daf035393b370c62fc7fb567f7fae133d Mon Sep 17 00:00:00 2001 From: Gianluca Cannata Date: Tue, 3 Mar 2026 15:19:47 +0100 Subject: [PATCH 3/3] fix: resolve golangci-lint issues in vpc commands - Fix gofmt alignment in var blocks for vpc_firewall_create.go and vpc_loadbalancer_update.go - Simplify createRules assignment to satisfy staticcheck QF1007 --- cmd/vpc/vpc_firewall_create.go | 7 ++----- cmd/vpc/vpc_loadbalancer_update.go | 18 +++++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/cmd/vpc/vpc_firewall_create.go b/cmd/vpc/vpc_firewall_create.go index cf3cf828..7506b4b5 100644 --- a/cmd/vpc/vpc_firewall_create.go +++ b/cmd/vpc/vpc_firewall_create.go @@ -12,7 +12,7 @@ import ( ) var ( - vpcFwNetwork string + vpcFwNetwork string vpcFwNoDefaultRules bool ) @@ -51,10 +51,7 @@ var vpcFirewallCreateCmd = &cobra.Command{ networkID = network.ID } - createRules := true - if vpcFwNoDefaultRules { - createRules = false - } + createRules := !vpcFwNoDefaultRules firewall, err := client.NewVPCFirewall(&civogo.FirewallConfig{ Name: args[0], diff --git a/cmd/vpc/vpc_loadbalancer_update.go b/cmd/vpc/vpc_loadbalancer_update.go index 2ed91513..b1f9d9ca 100644 --- a/cmd/vpc/vpc_loadbalancer_update.go +++ b/cmd/vpc/vpc_loadbalancer_update.go @@ -12,15 +12,15 @@ import ( ) var ( - vpcLBUpdateName string - vpcLBUpdateAlgorithm string - vpcLBUpdateExternalTrafficPolicy string - vpcLBUpdateSessionAffinity string - vpcLBUpdateSessionAffinityTimeout int32 - vpcLBUpdateEnableProxyProtocol string - vpcLBUpdateFirewallID string - vpcLBUpdateMaxConcurrentRequests int - vpcLBUpdateBackends []string + vpcLBUpdateName string + vpcLBUpdateAlgorithm string + vpcLBUpdateExternalTrafficPolicy string + vpcLBUpdateSessionAffinity string + vpcLBUpdateSessionAffinityTimeout int32 + vpcLBUpdateEnableProxyProtocol string + vpcLBUpdateFirewallID string + vpcLBUpdateMaxConcurrentRequests int + vpcLBUpdateBackends []string ) var vpcLoadBalancerUpdateCmd = &cobra.Command{