diff --git a/.goreleaser.yml b/.goreleaser.yml index 606f0ce6..c42dae1c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,7 +3,7 @@ builds: main: ./cmd/hcloud/main.go binary: hcloud ldflags: - - -w -X github.com/hetznercloud/cli/cli.Version=v{{.Version}} + - -w -X github.com/hetznercloud/cli/cli.Version={{.Version}} env: - CGO_ENABLED=0 goos: @@ -23,7 +23,7 @@ builds: main: ./cmd/hcloud/main.go binary: hcloud ldflags: - - -w -X github.com/hetznercloud/cli/cli.Version=v{{.Version}} + - -w -X github.com/hetznercloud/cli/cli.Version={{.Version}} env: - CGO_ENABLED=0 goos: diff --git a/cli/certificate.go b/cli/certificate.go new file mode 100644 index 00000000..51dc4f23 --- /dev/null +++ b/cli/certificate.go @@ -0,0 +1,29 @@ +package cli + +import "github.com/spf13/cobra" + +func newCertificatesCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "certificate", + Short: "Manage certificates", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableFlagsInUseLine: true, + RunE: cli.wrap(runCertificates), + } + cmd.AddCommand( + newCertificatesListCommand(cli), + newCertificateCreateCommand(cli), + newCertificateUpdateCommand(cli), + newCertificateAddLabelCommand(cli), + newCertificateRemoveLabelCommand(cli), + newCertificateDeleteCommand(cli), + newCertificateDescribeCommand(cli), + ) + + return cmd +} + +func runCertificates(cli *CLI, cmd *cobra.Command, args []string) error { + return cmd.Usage() +} diff --git a/cli/certificate_add_label.go b/cli/certificate_add_label.go new file mode 100644 index 00000000..80fe4c7f --- /dev/null +++ b/cli/certificate_add_label.go @@ -0,0 +1,64 @@ +package cli + +import ( + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newCertificateAddLabelCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-label [FLAGS] CERTIFICATE LABEL", + Short: "Add a label to a certificate", + Args: cobra.ExactArgs(2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: chainRunE(validateCertificateAddLabel, cli.ensureToken), + RunE: cli.wrap(runCertificateAddLabel), + } + + cmd.Flags().BoolP("overwrite", "o", false, "Overwrite label if it exists already") + return cmd +} + +func validateCertificateAddLabel(cmd *cobra.Command, args []string) error { + label := splitLabel(args[1]) + if len(label) != 2 { + return fmt.Errorf("invalid label: %s", args[1]) + } + return nil +} + +func runCertificateAddLabel(cli *CLI, cmd *cobra.Command, args []string) error { + overwrite, err := cmd.Flags().GetBool("overwrite") + if err != nil { + return err + } + idOrName := args[0] + cert, _, err := cli.Client().Certificate.Get(cli.Context, idOrName) + if err != nil { + return err + } + if cert == nil { + return fmt.Errorf("Certificate not found: %s", idOrName) + } + label := splitLabel(args[1]) + if _, ok := cert.Labels[label[0]]; ok && !overwrite { + return fmt.Errorf("Label %s on certificate %d already exists", label[0], cert.ID) + } + if cert.Labels == nil { + cert.Labels = make(map[string]string) + } + labels := cert.Labels + labels[label[0]] = label[1] + opts := hcloud.CertificateUpdateOpts{ + Labels: labels, + } + _, _, err = cli.Client().Certificate.Update(cli.Context, cert, opts) + if err != nil { + return err + } + fmt.Printf("Label %s added to certificate %d\n", label[0], cert.ID) + return nil +} diff --git a/cli/certificate_create.go b/cli/certificate_create.go new file mode 100644 index 00000000..3b776f3e --- /dev/null +++ b/cli/certificate_create.go @@ -0,0 +1,71 @@ +package cli + +import ( + "fmt" + "io/ioutil" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newCertificateCreateCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "create [FLAGS]", + Short: "Create or upload a Certificate", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runCertificateCreate), + } + + cmd.Flags().String("name", "", "Certificate name") + cmd.MarkFlagRequired("name") + + cmd.Flags().String("cert-file", "", "File containing the PEM encoded certificate") + cmd.MarkFlagRequired("cert-file") + + cmd.Flags().String("key-file", "", "File containing the PEM encoded private key for the certificate") + cmd.MarkFlagRequired("key-file") + + return cmd +} + +func runCertificateCreate(cli *CLI, cmd *cobra.Command, args []string) error { + var ( + name string + + certFile, keyFile string + certPEM, keyPEM []byte + cert *hcloud.Certificate + + err error + ) + if name, err = cmd.Flags().GetString("name"); err != nil { + return err + } + if certFile, err = cmd.Flags().GetString("cert-file"); err != nil { + return err + } + if keyFile, err = cmd.Flags().GetString("key-file"); err != nil { + return err + } + + if certPEM, err = ioutil.ReadFile(certFile); err != nil { + return err + } + if keyPEM, err = ioutil.ReadFile(keyFile); err != nil { + return err + } + + createOpts := hcloud.CertificateCreateOpts{ + Certificate: string(certPEM), + Name: name, + PrivateKey: string(keyPEM), + } + if cert, _, err = cli.Client().Certificate.Create(cli.Context, createOpts); err != nil { + return err + } + fmt.Printf("Certificate %d created\n", cert.ID) + return nil +} diff --git a/cli/certificate_delete.go b/cli/certificate_delete.go new file mode 100644 index 00000000..4ac3e454 --- /dev/null +++ b/cli/certificate_delete.go @@ -0,0 +1,36 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newCertificateDeleteCommand(cli *CLI) *cobra.Command { + return &cobra.Command{ + Use: "delete CERTIFICATE", + Short: "Delete a certificate", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runCertificateDelete), + } +} + +func runCertificateDelete(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + cert, _, err := cli.Client().Certificate.Get(cli.Context, idOrName) + if err != nil { + return err + } + if cert == nil { + return fmt.Errorf("Certificate %s not found", idOrName) + } + _, err = cli.Client().Certificate.Delete(cli.Context, cert) + if err != nil { + return err + } + fmt.Printf("Certificate %d deleted\n", cert.ID) + return nil +} diff --git a/cli/certificate_describe.go b/cli/certificate_describe.go new file mode 100644 index 00000000..81d50a94 --- /dev/null +++ b/cli/certificate_describe.go @@ -0,0 +1,82 @@ +package cli + +import ( + "encoding/json" + "fmt" + + humanize "github.com/dustin/go-humanize" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newCertificateDescribeCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe [FLAGS] CERTIFICATE", + Short: "Describe a certificate", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runCertificateDescribe), + } + addOutputFlag(cmd, outputOptionJSON(), outputOptionFormat()) + return cmd +} + +func runCertificateDescribe(cli *CLI, cmd *cobra.Command, args []string) error { + outputFlags := outputFlagsForCommand(cmd) + + idOrName := args[0] + cert, resp, err := cli.Client().Certificate.Get(cli.Context, idOrName) + if err != nil { + return err + } + if cert == nil { + return fmt.Errorf("certificate not found: %s", idOrName) + } + + switch { + case outputFlags.IsSet("json"): + return certificateDescribeJSON(resp) + case outputFlags.IsSet("format"): + return describeFormat(cert, outputFlags["format"][0]) + default: + return certificateDescribeText(cli, cert) + } +} + +func certificateDescribeJSON(resp *hcloud.Response) error { + var data map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + if server, ok := data["certificate"]; ok { + return describeJSON(server) + } + if servers, ok := data["certificates"].([]interface{}); ok { + return describeJSON(servers[0]) + } + return describeJSON(data) +} + +func certificateDescribeText(cli *CLI, cert *hcloud.Certificate) error { + fmt.Printf("ID:\t\t\t%d\n", cert.ID) + fmt.Printf("Name:\t\t\t%s\n", cert.Name) + fmt.Printf("Fingerprint:\t\t%s\n", cert.Fingerprint) + fmt.Printf("Created:\t\t%s (%s)\n", datetime(cert.Created), humanize.Time(cert.Created)) + fmt.Printf("Not valid before:\t%s (%s)\n", datetime(cert.NotValidBefore), humanize.Time(cert.NotValidBefore)) + fmt.Printf("Not valid after:\t%s (%s)\n", datetime(cert.NotValidAfter), humanize.Time(cert.NotValidAfter)) + fmt.Printf("Domain names:\n") + for _, domainName := range cert.DomainNames { + fmt.Printf(" - %s\n", domainName) + } + fmt.Print("Labels:\n") + if len(cert.Labels) == 0 { + fmt.Print(" No labels\n") + } else { + for key, value := range cert.Labels { + fmt.Printf(" %s:\t%s\n", key, value) + } + } + return nil +} diff --git a/cli/certificate_list.go b/cli/certificate_list.go new file mode 100644 index 00000000..1905da74 --- /dev/null +++ b/cli/certificate_list.go @@ -0,0 +1,113 @@ +package cli + +import ( + "strings" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/hcloud-go/hcloud/schema" + "github.com/spf13/cobra" +) + +var certficateTableOutput *tableOutput + +func init() { + certficateTableOutput = describeCertificatesTableOutput() +} + +func newCertificatesListCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "list [FLAGS]", + Short: "List Certificates", + Long: listLongDescription( + "Displays a list of certificates", + certficateTableOutput.Columns(), + ), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runCertificatesList), + } + addOutputFlag(cmd, outputOptionNoHeader(), outputOptionColumns(serverListTableOutput.Columns()), outputOptionJSON()) + return cmd +} + +func runCertificatesList(cli *CLI, cmd *cobra.Command, args []string) error { + outOpts := outputFlagsForCommand(cmd) + + labelSelector, _ := cmd.Flags().GetString("selector") + + opts := hcloud.CertificateListOpts{ + ListOpts: hcloud.ListOpts{ + LabelSelector: labelSelector, + PerPage: 50, + }, + } + + certs, err := cli.Client().Certificate.AllWithOpts(cli.Context, opts) + if err != nil { + return err + } + + if outOpts.IsSet("json") { + var certSchemas []schema.Certificate + + for _, cert := range certs { + certSchema := schema.Certificate{ + ID: cert.ID, + Certificate: cert.Certificate, + Created: cert.Created, + DomainNames: cert.DomainNames, + Fingerprint: cert.Fingerprint, + Labels: cert.Labels, + Name: cert.Name, + NotValidAfter: cert.NotValidAfter, + NotValidBefore: cert.NotValidBefore, + } + certSchemas = append(certSchemas, certSchema) + } + + return describeJSON(certSchemas) + } + + cols := []string{"id", "name", "domain_names", "not_valid_after"} + if outOpts.IsSet("columns") { + cols = outOpts["columns"] + } + tw := describeCertificatesTableOutput() + if err := tw.ValidateColumns(cols); err != nil { + return nil + } + if !outOpts.IsSet("noheader") { + tw.WriteHeader(cols) + } + for _, cert := range certs { + tw.Write(cols, cert) + } + return tw.Flush() +} + +func describeCertificatesTableOutput() *tableOutput { + return newTableOutput(). + AddAllowedFields(hcloud.Certificate{}). + RemoveAllowedField("certificate", "chain"). + AddFieldOutputFn("labels", fieldOutputFn(func(obj interface{}) string { + cert := obj.(*hcloud.Certificate) + return labelsToString(cert.Labels) + })). + AddFieldOutputFn("not_valid_before", func(obj interface{}) string { + cert := obj.(*hcloud.Certificate) + return datetime(cert.NotValidBefore) + }). + AddFieldOutputFn("not_valid_after", func(obj interface{}) string { + cert := obj.(*hcloud.Certificate) + return datetime(cert.NotValidAfter) + }). + AddFieldOutputFn("domain_names", func(obj interface{}) string { + cert := obj.(*hcloud.Certificate) + return strings.Join(cert.DomainNames, ", ") + }). + AddFieldOutputFn("created", fieldOutputFn(func(obj interface{}) string { + cert := obj.(*hcloud.Certificate) + return datetime(cert.Created) + })) +} diff --git a/cli/certificate_remove_label.go b/cli/certificate_remove_label.go new file mode 100644 index 00000000..b267d7e1 --- /dev/null +++ b/cli/certificate_remove_label.go @@ -0,0 +1,74 @@ +package cli + +import ( + "errors" + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newCertificateRemoveLabelCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove-label [FLAGS] CERTIFICATE LABELKEY", + Short: "Remove a label from a certificate", + Args: cobra.RangeArgs(1, 2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: chainRunE(validateCertificateRemoveLabel, cli.ensureToken), + RunE: cli.wrap(runCertificateRemoveLabel), + } + cmd.Flags().BoolP("all", "a", false, "Remove all labels") + return cmd +} + +func validateCertificateRemoveLabel(cmd *cobra.Command, args []string) error { + all, err := cmd.Flags().GetBool("all") + if err != nil { + return err + } + if all && len(args) != 1 { + return errors.New("must not specify a label key when using --all/-a") + } + if !all && len(args) != 2 { + return errors.New("must specify a label key when not using --all/-a") + } + return nil +} + +func runCertificateRemoveLabel(cli *CLI, cmd *cobra.Command, args []string) error { + // We ensured the all flag is a valid boolean in + // validateCertificateRemoveLabel. No need to handle the error again here. + all, _ := cmd.Flags().GetBool("all") + idOrName := args[0] + cert, _, err := cli.Client().Certificate.Get(cli.Context, idOrName) + if err != nil { + return err + } + if cert == nil { + return fmt.Errorf("Certificate not found: %s", idOrName) + } + if all { + cert.Labels = make(map[string]string) + } else { + label := args[1] + if _, ok := cert.Labels[label]; !ok { + return fmt.Errorf("Label %s on certificate %d does not exist", label, cert.ID) + } + delete(cert.Labels, label) + } + opts := hcloud.CertificateUpdateOpts{ + Labels: cert.Labels, + } + _, _, err = cli.Client().Certificate.Update(cli.Context, cert, opts) + if err != nil { + return err + } + + if all { + fmt.Printf("All labels removed from certificate %d\n", cert.ID) + } else { + fmt.Printf("Label %s removed from certificate %d\n", args[1], cert.ID) + } + return nil +} diff --git a/cli/certificate_update.go b/cli/certificate_update.go new file mode 100644 index 00000000..fe6ae827 --- /dev/null +++ b/cli/certificate_update.go @@ -0,0 +1,47 @@ +package cli + +import ( + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newCertificateUpdateCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "update [FLAGS] CERTFICATE", + Short: "Update an existing Certificate", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runCertificateUpdate), + } + + cmd.Flags().String("name", "", "Certificate name") + return cmd +} + +func runCertificateUpdate(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + cert, _, err := cli.Client().Certificate.Get(cli.Context, idOrName) + if err != nil { + return err + } + if cert == nil { + return fmt.Errorf("Certificate %s not found", idOrName) + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + updOpts := hcloud.CertificateUpdateOpts{ + Name: name, + } + _, _, err = cli.Client().Certificate.Update(cli.Context, cert, updOpts) + if err != nil { + return err + } + fmt.Printf("Certificate %d updated\n", cert.ID) + return nil +} diff --git a/cli/completion.go b/cli/completion.go index 98ab4c60..1b7fa20b 100644 --- a/cli/completion.go +++ b/cli/completion.go @@ -83,6 +83,20 @@ const ( fi } + __hcloud_load_balancer_names() { + local ctl_output out + if ctl_output=$(hcloud load-balancer list -o noheader -o columns=name 2>/dev/null); then + COMPREPLY=($(echo "${ctl_output}")) + fi + } + + __hcloud_load_balancer_type_names() { + local ctl_output out + if ctl_output=$(hcloud load-balancer-type list -o noheader -o columns=name 2>/dev/null); then + COMPREPLY=($(echo "${ctl_output}")) + fi + } + __hcloud_image_ids_no_system() { local ctl_output out if ctl_output=$(hcloud image list -o noheader 2>/dev/null); then @@ -104,10 +118,21 @@ const ( fi } + __hcloud_certificate_names() { + local ctl_output out + if ctl_output=$(hcloud certificate list -o noheader 2>/dev/null); then + COMPREPLY=($(echo "${ctl_output}" | awk '{print $2}')) + fi + } + __hcloud_image_types_no_system() { COMPREPLY=($(echo "snapshot backup")) } + __hcloud_load_balancer_algorithm_types() { + COMPREPLY=($(echo "round_robin least_connections")) + } + __hcloud_protection_levels() { COMPREPLY=($(echo "delete")) } @@ -173,6 +198,49 @@ const ( __hcloud_servertype_names return ;; + hcloud_load-balancer-type_describe ) + __hcloud_load_balancer_type_names + return + ;; + hcloud_load-balancer_delete | hcloud_load-balancer_describe | \ + hcloud_load-balancer_update | hcloud_load-balancer_add-label | \ + hcloud_load-balancer_remove-label | hcloud_load-balancer_enable-public-interface | \ + hcloud_load-balancer_disable-public-interface ) + __hcloud_load_balancer_names + return + ;; + hcloud_load-balancer_enable-protection | hcloud_load-balancer_disable-protection ) + if [[ ${#nouns[@]} -gt 1 ]]; then + return 1 + fi + if [[ ${#nouns[@]} -eq 1 ]]; then + __hcloud_protection_levels + return + fi + __hcloud_load_balancer_names + return + ;; + hcloud_load-balancer_change-algorithm ) + if [[ ${#nouns[@]} -gt 1 ]]; then + return 1 + fi + if [[ ${#nouns[@]} -eq 1 ]]; then + __hcloud_load_balancer_algorithm_types + return + fi + __hcloud_load_balancer_names + return + ;; + hcloud_load-balancer_add-target | hcloud_load-balancer_update-service | \ + hcloud_load-balancer_remove-target | hcloud_load-balancer_add-service | \ + hcloud_load-balancer_delete-service | hcloud_load-balancer_update-health-check | \ + hcloud_load-balancer_attach-to-network | hcloud_load-balancer_detach-from-network ) + if [[ ${#nouns[@]} -gt 1 ]]; then + return 1 + fi + __hcloud_load_balancer_names + return + ;; hcloud_image_describe | hcloud_image_add-label | hcloud_image_remove-label ) __hcloud_image_names return @@ -260,6 +328,10 @@ const ( __hcloud_iso_names return ;; + hcloud_load-balancer_describe ) + __hcloud_load_balancer_names + return + ;; hcloud_context_use | hcloud_context_delete ) __hcloud_context_names return @@ -269,6 +341,12 @@ const ( __hcloud_sshkey_names return ;; + hcloud_certificate_describe | hcloud_certificate_update | \ + hcloud_certificate_add-label | hcloud_certificate_remove-label | \ + hcloud_certificate_delete ) + __hcloud_certificate_names + return + ;; *) ;; esac diff --git a/cli/load_balancer.go b/cli/load_balancer.go new file mode 100644 index 00000000..75eac005 --- /dev/null +++ b/cli/load_balancer.go @@ -0,0 +1,40 @@ +package cli + +import "github.com/spf13/cobra" + +func newLoadBalancerCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "load-balancer", + Short: "Manage Load Balancers", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableFlagsInUseLine: true, + RunE: cli.wrap(runLoadBalancer), + } + cmd.AddCommand( + newLoadBalancerCreateCommand(cli), + newLoadBalancerListCommand(cli), + newLoadBalancerDescribeCommand(cli), + newLoadBalancerDeleteCommand(cli), + newLoadBalancerUpdateCommand(cli), + newLoadBalancerAddLabelCommand(cli), + newLoadBalancerRemoveLabelCommand(cli), + newLoadBalancerAddTargetCommand(cli), + newLoadBalancerRemoveTargetCommand(cli), + newLoadBalancerChangeAlgorithmCommand(cli), + newLoadBalancerUpdateServiceCommand(cli), + newLoadBalancerDeleteServiceCommand(cli), + newLoadBalancerAddServiceCommand(cli), + newLoadBalancerEnableProtectionCommand(cli), + newLoadBalancerDisableProtectionCommand(cli), + newLoadBalancerAttachToNetworkCommand(cli), + newLoadBalancerDetachFromNetworkCommand(cli), + newLoadBalancerEnablePublicInterface(cli), + newLoadBalancerDisablePublicInterface(cli), + ) + return cmd +} + +func runLoadBalancer(cli *CLI, cmd *cobra.Command, args []string) error { + return cmd.Usage() +} diff --git a/cli/load_balancer_add_label.go b/cli/load_balancer_add_label.go new file mode 100644 index 00000000..6101591f --- /dev/null +++ b/cli/load_balancer_add_label.go @@ -0,0 +1,61 @@ +package cli + +import ( + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerAddLabelCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-label [FLAGS] LOADBALANCER LABEL", + Short: "Add a label to a Load Balancer", + Args: cobra.ExactArgs(2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: chainRunE(validateLoadBalancerAddLabel, cli.ensureToken), + RunE: cli.wrap(runLoadBalancerAddLabel), + } + + cmd.Flags().BoolP("overwrite", "o", false, "Overwrite label if it exists already") + return cmd +} + +func validateLoadBalancerAddLabel(cmd *cobra.Command, args []string) error { + label := splitLabel(args[1]) + if len(label) != 2 { + return fmt.Errorf("invalid label: %s", args[1]) + } + + return nil +} + +func runLoadBalancerAddLabel(cli *CLI, cmd *cobra.Command, args []string) error { + overwrite, _ := cmd.Flags().GetBool("overwrite") + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + label := splitLabel(args[1]) + + if _, ok := loadBalancer.Labels[label[0]]; ok && !overwrite { + return fmt.Errorf("label %s on Load Balancer %d already exists", label[0], loadBalancer.ID) + } + labels := loadBalancer.Labels + labels[label[0]] = label[1] + opts := hcloud.LoadBalancerUpdateOpts{ + Labels: labels, + } + _, _, err = cli.Client().LoadBalancer.Update(cli.Context, loadBalancer, opts) + if err != nil { + return err + } + fmt.Printf("Label %s added to Load Balancer %d\n", label[0], loadBalancer.ID) + + return nil +} diff --git a/cli/load_balancer_add_service.go b/cli/load_balancer_add_service.go new file mode 100644 index 00000000..5ceba179 --- /dev/null +++ b/cli/load_balancer_add_service.go @@ -0,0 +1,132 @@ +package cli + +import ( + "fmt" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerAddServiceCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-service LOADBALANCER FLAGS", + Short: "Add a service from a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: chainRunE(validateLoadBalancerAddService, cli.ensureToken), + RunE: cli.wrap(runLoadBalancerAddService), + } + cmd.Flags().String("protocol", "", "Protocol of the service") + cmd.MarkFlagRequired("protocol") + + cmd.Flags().Int("listen-port", 0, "Listen port of the service") + cmd.Flags().Int("destination-port", 0, "Destination port of the service on the targets") + cmd.Flags().Bool("proxy-protocol", false, "Enable proxyprotocol") + + cmd.Flags().Bool("http-sticky-sessions", false, "Enable Sticky Sessions") + cmd.Flags().String("http-cookie-name", "", "Sticky Sessions: Cookie Name we set") + cmd.Flags().Duration("http-cookie-lifetime", 0, "Sticky Sessions: Lifetime of the cookie") + cmd.Flags().IntSlice("http-certificates", []int{}, "ID of Certificates which are attached to this Load Balancer") + cmd.Flags().Bool("http-redirect-http", false, "Redirect all traffic on port 80 to port 443") + + return cmd +} + +func validateLoadBalancerAddService(cmd *cobra.Command, args []string) error { + protocol, _ := cmd.Flags().GetString("protocol") + listenPort, _ := cmd.Flags().GetInt("listen-port") + destinationPort, _ := cmd.Flags().GetInt("destination-port") + httpCertificates, _ := cmd.Flags().GetIntSlice("http-certificates") + + if protocol == "" { + return fmt.Errorf("required flag protocol not set") + } + + switch hcloud.LoadBalancerServiceProtocol(protocol) { + case hcloud.LoadBalancerServiceProtocolHTTP: + break + case hcloud.LoadBalancerServiceProtocolTCP: + if listenPort == 0 { + return fmt.Errorf("please specify a listen port") + } + + if destinationPort == 0 { + return fmt.Errorf("please specify a destination port") + } + break + case hcloud.LoadBalancerServiceProtocolHTTPS: + if len(httpCertificates) == 0 { + return fmt.Errorf("no certificate specified") + } + default: + return fmt.Errorf("%s is not a valid protocol", protocol) + } + if listenPort > 65535 { + return fmt.Errorf("%d is not a valid listen port", listenPort) + } + + if destinationPort > 65535 { + return fmt.Errorf("%d is not a valid destination port", destinationPort) + } + return nil +} + +func runLoadBalancerAddService(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + protocol, _ := cmd.Flags().GetString("protocol") + listenPort, _ := cmd.Flags().GetInt("listen-port") + destinationPort, _ := cmd.Flags().GetInt("destination-port") + proxyProtocol, _ := cmd.Flags().GetBool("proxy-protocol") + + httpStickySessions, _ := cmd.Flags().GetBool("http-sticky-sessions") + httpCookieName, _ := cmd.Flags().GetString("http-cookie-name") + httpCookieLifetime, _ := cmd.Flags().GetDuration("http-cookie-lifetime") + httpCertificates, _ := cmd.Flags().GetIntSlice("http-certificates") + httpRedirect, _ := cmd.Flags().GetBool("http-redirect-http") + + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + opts := hcloud.LoadBalancerAddServiceOpts{ + Protocol: hcloud.LoadBalancerServiceProtocol(protocol), + Proxyprotocol: hcloud.Bool(proxyProtocol), + } + + if listenPort != 0 { + opts.ListenPort = hcloud.Int(listenPort) + } + if destinationPort != 0 { + opts.DestinationPort = hcloud.Int(destinationPort) + } + + if protocol != string(hcloud.LoadBalancerServiceProtocolTCP) { + opts.HTTP = &hcloud.LoadBalancerAddServiceOptsHTTP{ + StickySessions: hcloud.Bool(httpStickySessions), + RedirectHTTP: hcloud.Bool(httpRedirect), + } + if httpCookieName != "" { + opts.HTTP.CookieName = hcloud.String(httpCookieName) + } + if httpCookieLifetime != 0 { + opts.HTTP.CookieLifetime = hcloud.Duration(httpCookieLifetime) + } + for _, certificateID := range httpCertificates { + opts.HTTP.Certificates = append(opts.HTTP.Certificates, &hcloud.Certificate{ID: certificateID}) + } + } + action, _, err := cli.Client().LoadBalancer.AddService(cli.Context, loadBalancer, opts) + if err != nil { + return err + } + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + fmt.Printf("Service was added to Load Balancer %d\n", loadBalancer.ID) + + return nil +} diff --git a/cli/load_balancer_add_target.go b/cli/load_balancer_add_target.go new file mode 100644 index 00000000..f76c57c9 --- /dev/null +++ b/cli/load_balancer_add_target.go @@ -0,0 +1,67 @@ +package cli + +import ( + "fmt" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerAddTargetCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-target LOADBALANCER FLAGS", + Short: "Add a target to a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerAddTarget), + } + + cmd.Flags().String("server", "", "Name or ID of the server") + cmd.Flag("server").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_server_names"}, + } + cmd.Flags().Bool("use-private-ip", false, "Determine if the Load Balancer should connect to the target via the network") + return cmd +} + +func runLoadBalancerAddTarget(cli *CLI, cmd *cobra.Command, args []string) error { + serverIdOrName, _ := cmd.Flags().GetString("server") + idOrName := args[0] + usePrivateIP, _ := cmd.Flags().GetBool("use-private-ip") + + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + var action *hcloud.Action + if serverIdOrName != "" { + server, _, err := cli.Client().Server.Get(cli.Context, serverIdOrName) + if err != nil { + return err + } + if server == nil { + return fmt.Errorf("server not found: %s", serverIdOrName) + } + action, _, err = cli.Client().LoadBalancer.AddServerTarget(cli.Context, loadBalancer, hcloud.LoadBalancerAddServerTargetOpts{ + Server: server, + UsePrivateIP: hcloud.Bool(usePrivateIP), + }) + if err != nil { + return err + } + } else { + return fmt.Errorf("specify one of server") + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + fmt.Printf("Target added to Load Balancer %d\n", loadBalancer.ID) + + return nil +} diff --git a/cli/load_balancer_attach_to_network.go b/cli/load_balancer_attach_to_network.go new file mode 100644 index 00000000..efb033f3 --- /dev/null +++ b/cli/load_balancer_attach_to_network.go @@ -0,0 +1,68 @@ +package cli + +import ( + "fmt" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerAttachToNetworkCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "attach-to-network [FLAGS] LOADBALANCER", + Short: "Attach a Load Balancer to a Network", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerAttachToNetwork), + } + + cmd.Flags().StringP("network", "n", "", "Network (ID or name)") + cmd.Flag("network").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_network_names"}, + } + cmd.MarkFlagRequired("network") + + cmd.Flags().IP("ip", nil, "IP address to assign to the Load Balancer (auto-assigned if omitted)") + + return cmd +} + +func runLoadBalancerAttachToNetwork(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + networkIDOrName, _ := cmd.Flags().GetString("network") + network, _, err := cli.Client().Network.Get(cli.Context, networkIDOrName) + if err != nil { + return err + } + if network == nil { + return fmt.Errorf("network not found: %s", networkIDOrName) + } + + ip, _ := cmd.Flags().GetIP("ip") + + opts := hcloud.LoadBalancerAttachToNetworkOpts{ + Network: network, + IP: ip, + } + action, _, err := cli.Client().LoadBalancer.AttachToNetwork(cli.Context, loadBalancer, opts) + + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + + fmt.Printf("Load Balancer %d attached to network %d\n", loadBalancer.ID, network.ID) + return nil +} diff --git a/cli/load_balancer_change_algorithm.go b/cli/load_balancer_change_algorithm.go new file mode 100644 index 00000000..8f1cff37 --- /dev/null +++ b/cli/load_balancer_change_algorithm.go @@ -0,0 +1,49 @@ +package cli + +import ( + "fmt" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerChangeAlgorithmCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "change-algorithm LOADBALANCER FLAGS", + Short: "Changes the algorithm of a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerChangeAlgorithm), + } + + cmd.Flags().String("algorithm-type", "", "The new algorithm of the Load Balancer") + cmd.Flag("algorithm-type").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_load_balancer_algorithm_types"}, + } + cmd.MarkFlagRequired("algorithm-type") + return cmd +} + +func runLoadBalancerChangeAlgorithm(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + algorithm, _ := cmd.Flags().GetString("algorithm-type") + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + action, _, err := cli.Client().LoadBalancer.ChangeAlgorithm(cli.Context, loadBalancer, hcloud.LoadBalancerChangeAlgorithmOpts{Type: hcloud.LoadBalancerAlgorithmType(algorithm)}) + if err != nil { + return err + } + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + fmt.Printf("Algorithm for Load Balancer %d was changed\n", loadBalancer.ID) + + return nil +} diff --git a/cli/load_balancer_create.go b/cli/load_balancer_create.go new file mode 100644 index 00000000..d49bd1ba --- /dev/null +++ b/cli/load_balancer_create.go @@ -0,0 +1,90 @@ +package cli + +import ( + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerCreateCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "create [FLAGS]", + Short: "Create a Load Balancer", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerCreate), + } + + cmd.Flags().String("name", "", "Load Balancer name") + cmd.MarkFlagRequired("name") + + cmd.Flags().String("type", "", "Load Balancer type (ID or name)") + cmd.Flag("type").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_load_balancer_type_names"}, + } + cmd.MarkFlagRequired("type") + + cmd.Flags().String("algorithm-type", "", "Algorithm Type name (round_robin or least_connections)") + + cmd.Flag("algorithm-type").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_load_balancer_algorithm_types"}, + } + cmd.Flags().String("location", "", "Location (ID or name)") + cmd.Flag("location").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_location_names"}, + } + + cmd.Flags().String("network-zone", "", "Network Zone") + cmd.Flag("network-zone").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_network_zones"}, + } + + cmd.Flags().StringToString("label", nil, "User-defined labels ('key=value') (can be specified multiple times)") + + return cmd +} + +func runLoadBalancerCreate(cli *CLI, cmd *cobra.Command, args []string) error { + name, _ := cmd.Flags().GetString("name") + serverType, _ := cmd.Flags().GetString("type") + algorithmType, _ := cmd.Flags().GetString("algorithm-type") + location, _ := cmd.Flags().GetString("location") + networkZone, _ := cmd.Flags().GetString("network-zone") + labels, _ := cmd.Flags().GetStringToString("label") + + opts := hcloud.LoadBalancerCreateOpts{ + Name: name, + LoadBalancerType: &hcloud.LoadBalancerType{ + Name: serverType, + }, + Labels: labels, + } + if algorithmType != "" { + opts.Algorithm = &hcloud.LoadBalancerAlgorithm{Type: hcloud.LoadBalancerAlgorithmType(algorithmType)} + } + if networkZone != "" { + opts.NetworkZone = hcloud.NetworkZone(networkZone) + } + if location != "" { + opts.Location = &hcloud.Location{Name: location} + } + result, _, err := cli.Client().LoadBalancer.Create(cli.Context, opts) + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, result.Action); err != nil { + return err + } + loadBalancer, _, err := cli.Client().LoadBalancer.GetByID(cli.Context, result.LoadBalancer.ID) + if err != nil { + return err + } + fmt.Printf("LoadBalancer %d created\n", loadBalancer.ID) + fmt.Printf("IPv4: %s\n", loadBalancer.PublicNet.IPv4.IP.String()) + fmt.Printf("IPv6: %s\n", loadBalancer.PublicNet.IPv6.IP.String()) + return nil +} diff --git a/cli/load_balancer_delete.go b/cli/load_balancer_delete.go new file mode 100644 index 00000000..3a10395a --- /dev/null +++ b/cli/load_balancer_delete.go @@ -0,0 +1,39 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newLoadBalancerDeleteCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [FLAGS] LOADBALANCER", + Short: "Delete a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerDelete), + } + return cmd +} + +func runLoadBalancerDelete(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load balancer not found: %s", idOrName) + } + + _, err = cli.Client().LoadBalancer.Delete(cli.Context, loadBalancer) + if err != nil { + return err + } + + fmt.Printf("Load Balancer %d deleted\n", loadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_delete_service.go b/cli/load_balancer_delete_service.go new file mode 100644 index 00000000..70d71666 --- /dev/null +++ b/cli/load_balancer_delete_service.go @@ -0,0 +1,42 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newLoadBalancerDeleteServiceCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete-service [FLAGS] LOADBALANCER", + Short: "Deletes a service from a Load Balancer", + Args: cobra.RangeArgs(1, 2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: chainRunE(cli.ensureToken), + RunE: cli.wrap(runLoadBalancerDeleteService), + } + + cmd.Flags().Int("listen-port", 0, "The listen port of the service you want to delete") + cmd.MarkFlagRequired("listen-port") + return cmd +} + +func runLoadBalancerDeleteService(cli *CLI, cmd *cobra.Command, args []string) error { + listenPort, _ := cmd.Flags().GetInt("listen-port") + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + _, _, err = cli.Client().LoadBalancer.DeleteService(cli.Context, loadBalancer, listenPort) + if err != nil { + return err + } + + fmt.Printf("Service on port %d deleted from Load Balancer %d\n", listenPort, loadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_describe.go b/cli/load_balancer_describe.go new file mode 100644 index 00000000..8e4ce39b --- /dev/null +++ b/cli/load_balancer_describe.go @@ -0,0 +1,151 @@ +package cli + +import ( + "fmt" + humanize "github.com/dustin/go-humanize" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerDescribeCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe [FLAGS] LOADBALANCER", + Short: "Describe a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerDescribe), + } + addOutputFlag(cmd, outputOptionJSON(), outputOptionFormat()) + //cmd.Flags().Bool("expand-targets", false, "Expand all label_selector targets") + return cmd +} + +func runLoadBalancerDescribe(cli *CLI, cmd *cobra.Command, args []string) error { + outputFlags := outputFlagsForCommand(cmd) + idOrName := args[0] + loadBalancer, resp, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("loadBalancer not found: %s", idOrName) + } + + switch { + case outputFlags.IsSet("json"): + return serverDescribeJSON(resp) + case outputFlags.IsSet("format"): + return describeFormat(loadBalancer, outputFlags["format"][0]) + default: + return loadBalancerDescribeText(cli, loadBalancer) + } +} + +func loadBalancerDescribeText(cli *CLI, loadBalancer *hcloud.LoadBalancer) error { + fmt.Printf("ID:\t\t\t\t%d\n", loadBalancer.ID) + fmt.Printf("Name:\t\t\t\t%s\n", loadBalancer.Name) + fmt.Printf("Created:\t\t\t%s (%s)\n", datetime(loadBalancer.Created), humanize.Time(loadBalancer.Created)) + fmt.Printf("Public Net:\n") + fmt.Printf(" Enabled:\t\t\t%s\n", yesno(loadBalancer.PublicNet.Enabled)) + fmt.Printf(" IPv4:\t\t\t\t%s\n", loadBalancer.PublicNet.IPv4.IP.String()) + fmt.Printf(" IPv6:\t\t\t\t%s\n", loadBalancer.PublicNet.IPv6.IP.String()) + + fmt.Printf("Private Net:\n") + if len(loadBalancer.PrivateNet) > 0 { + for _, n := range loadBalancer.PrivateNet { + network, _, err := cli.client.Network.GetByID(cli.Context, n.Network.ID) + if err != nil { + return fmt.Errorf("error fetching network: %v", err) + } + fmt.Printf(" - ID:\t\t\t%d\n", network.ID) + fmt.Printf(" Name:\t\t%s\n", network.Name) + fmt.Printf(" IP:\t\t\t%s\n", n.IP.String()) + } + } else { + fmt.Printf(" No Private Network\n") + } + fmt.Printf("Algorithm:\t\t\t%s\n", loadBalancer.Algorithm.Type) + + fmt.Printf("Load Balancer Type:\t\t%s (ID: %d)\n", loadBalancer.LoadBalancerType.Name, loadBalancer.LoadBalancerType.ID) + fmt.Printf(" ID:\t\t\t\t%d\n", loadBalancer.LoadBalancerType.ID) + fmt.Printf(" Name:\t\t\t\t%s\n", loadBalancer.LoadBalancerType.Name) + fmt.Printf(" Description:\t\t\t%s\n", loadBalancer.LoadBalancerType.Description) + fmt.Printf(" Max Services:\t\t\t%d\n", loadBalancer.LoadBalancerType.MaxServices) + fmt.Printf(" Max Connections:\t\t%d\n", loadBalancer.LoadBalancerType.MaxConnections) + fmt.Printf(" Max Targets:\t\t\t%d\n", loadBalancer.LoadBalancerType.MaxTargets) + fmt.Printf(" Max assigned Certificates:\t%d\n", loadBalancer.LoadBalancerType.MaxAssignedCertificates) + + fmt.Printf("Services:\n") + if len(loadBalancer.Services) == 0 { + fmt.Print(" No services\n") + } else { + for _, service := range loadBalancer.Services { + fmt.Printf(" - Protocol:\t\t\t%s\n", service.Protocol) + fmt.Printf(" Listen Port:\t\t%d\n", service.ListenPort) + fmt.Printf(" Destination Port:\t\t%d\n", service.DestinationPort) + fmt.Printf(" Proxy Protocol:\t\t%s\n", yesno(service.Proxyprotocol)) + if service.Protocol != hcloud.LoadBalancerServiceProtocolTCP { + fmt.Printf(" Sticky Sessions:\t\t%s\n", yesno(service.HTTP.StickySessions)) + if service.HTTP.StickySessions { + fmt.Printf(" Sticky Cookie Name:\t\t%s\n", service.HTTP.CookieName) + fmt.Printf(" Sticky Cookie Lifetime:\t%vs\n", service.HTTP.CookieLifetime.Seconds()) + } + if service.Protocol == hcloud.LoadBalancerServiceProtocolHTTPS { + fmt.Printf(" Certificates:\n") + for _, cert := range service.HTTP.Certificates { + fmt.Printf(" - ID: \t\t\t%v\n", cert.ID) + } + } + } + + fmt.Printf(" Health Check:\n") + fmt.Printf(" Protocol:\t\t\t%s\n", service.HealthCheck.Protocol) + fmt.Printf(" Timeout:\t\t\t%vs\n", service.HealthCheck.Timeout.Seconds()) + fmt.Printf(" Interval:\t\t\tevery %vs\n", service.HealthCheck.Interval.Seconds()) + fmt.Printf(" Retries:\t\t\t%d\n", service.HealthCheck.Retries) + if service.HealthCheck.Protocol != hcloud.LoadBalancerServiceProtocolTCP { + fmt.Printf(" HTTP Domain:\t\t%s\n", service.HealthCheck.HTTP.Domain) + fmt.Printf(" HTTP Path:\t\t%s\n", service.HealthCheck.HTTP.Path) + fmt.Printf(" Response:\t\t%s\n", service.HealthCheck.HTTP.Response) + fmt.Printf(" TLS:\t\t\t%s\n", yesno(service.HealthCheck.HTTP.TLS)) + fmt.Printf(" Status Codes:\t\t%v\n", service.HealthCheck.HTTP.StatusCodes) + } + } + } + + fmt.Printf("Targets:\n") + if len(loadBalancer.Targets) == 0 { + fmt.Print(" No targets\n") + } else { + for _, target := range loadBalancer.Targets { + fmt.Printf(" - Type:\t\t\t%s\n", target.Type) + if target.Server != nil { + fmt.Printf(" Server:\n") + fmt.Printf(" ID:\t\t\t%d\n", target.Server.Server.ID) + fmt.Printf(" Name:\t\t\t%s\n", cli.GetServerName(target.Server.Server.ID)) + fmt.Printf(" Use Private IP:\t\t%s\n", yesno(target.UsePrivateIP)) + fmt.Printf(" Status:\n") + for _, healthStatus := range target.HealthStatus { + fmt.Printf(" - Service:\t\t\t%d\n", healthStatus.ListenPort) + fmt.Printf(" Status:\t\t\t%s\n", healthStatus.Status) + } + } + } + } + + fmt.Printf("Protection:\n") + fmt.Printf(" Delete:\t%s\n", yesno(loadBalancer.Protection.Delete)) + + fmt.Print("Labels:\n") + if len(loadBalancer.Labels) == 0 { + fmt.Print(" No labels\n") + } else { + for key, value := range loadBalancer.Labels { + fmt.Printf(" %s: %s\n", key, value) + } + } + + return nil +} diff --git a/cli/load_balancer_detach_from_network.go b/cli/load_balancer_detach_from_network.go new file mode 100644 index 00000000..1f6d5f3d --- /dev/null +++ b/cli/load_balancer_detach_from_network.go @@ -0,0 +1,61 @@ +package cli + +import ( + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + + "github.com/spf13/cobra" +) + +func newLoadBalancerDetachFromNetworkCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "detach-from-network [FLAGS] LOADBALANCER", + Short: "Detach a Load Balancer from a Network", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerDetachFromNetwork), + } + cmd.Flags().StringP("network", "n", "", "Network (ID or name)") + cmd.Flag("network").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_network_names"}, + } + cmd.MarkFlagRequired("network") + return cmd +} + +func runLoadBalancerDetachFromNetwork(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + networkIDOrName, _ := cmd.Flags().GetString("network") + network, _, err := cli.Client().Network.Get(cli.Context, networkIDOrName) + if err != nil { + return err + } + if network == nil { + return fmt.Errorf("network not found: %s", networkIDOrName) + } + + opts := hcloud.LoadBalancerDetachFromNetworkOpts{ + Network: network, + } + action, _, err := cli.Client().LoadBalancer.DetachFromNetwork(cli.Context, loadBalancer, opts) + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + + fmt.Printf("Load Balancer %d detached from Network %d\n", loadBalancer.ID, network.ID) + return nil +} diff --git a/cli/load_balancer_disable_protection.go b/cli/load_balancer_disable_protection.go new file mode 100644 index 00000000..e63cee6f --- /dev/null +++ b/cli/load_balancer_disable_protection.go @@ -0,0 +1,59 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerDisableProtectionCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "disable-protection [FLAGS] LOADBALANCER PROTECTIONLEVEL [PROTECTIONLEVEL...]", + Short: "Disable resource protection for a Load Balancer", + Args: cobra.MinimumNArgs(2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerDisableProtection), + } + return cmd +} + +func runLoadBalancerDisableProtection(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + var unknown []string + opts := hcloud.LoadBalancerChangeProtectionOpts{} + for _, arg := range args[1:] { + switch strings.ToLower(arg) { + case "delete": + opts.Delete = hcloud.Bool(false) + default: + unknown = append(unknown, arg) + } + } + if len(unknown) > 0 { + return fmt.Errorf("unknown protection level: %s", strings.Join(unknown, ", ")) + } + + action, _, err := cli.Client().LoadBalancer.ChangeProtection(cli.Context, loadBalancer, opts) + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + + fmt.Printf("Resource protection disabled for Load Balancer %d\n", loadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_disable_public_interface.go b/cli/load_balancer_disable_public_interface.go new file mode 100644 index 00000000..6fa0c3c2 --- /dev/null +++ b/cli/load_balancer_disable_public_interface.go @@ -0,0 +1,44 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newLoadBalancerDisablePublicInterface(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "disable-public-interface [FLAGS] LOADBALANCER", + Short: "Disable the public interface of a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerDisablePublicInterface), + } + + return cmd +} + +func runLoadBalancerDisablePublicInterface(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + action, _, err := cli.Client().LoadBalancer.DisablePublicInterface(cli.Context, loadBalancer) + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + + fmt.Printf("Public interface of Load Balancer %d was disabled\n", loadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_enable_protection.go b/cli/load_balancer_enable_protection.go new file mode 100644 index 00000000..81383c04 --- /dev/null +++ b/cli/load_balancer_enable_protection.go @@ -0,0 +1,59 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerEnableProtectionCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "enable-protection [FLAGS] LOADBALANCER PROTECTIONLEVEL [PROTECTIONLEVEL...]", + Short: "Enable resource protection for a Load Balancer", + Args: cobra.MinimumNArgs(2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerEnableProtection), + } + return cmd +} + +func runLoadBalancerEnableProtection(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + LoadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if LoadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + var unknown []string + opts := hcloud.LoadBalancerChangeProtectionOpts{} + for _, arg := range args[1:] { + switch strings.ToLower(arg) { + case "delete": + opts.Delete = hcloud.Bool(true) + default: + unknown = append(unknown, arg) + } + } + if len(unknown) > 0 { + return fmt.Errorf("unknown protection level: %s", strings.Join(unknown, ", ")) + } + + action, _, err := cli.Client().LoadBalancer.ChangeProtection(cli.Context, LoadBalancer, opts) + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + + fmt.Printf("Resource protection enabled Load Balancer %d\n", LoadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_enable_public_interface.go b/cli/load_balancer_enable_public_interface.go new file mode 100644 index 00000000..254338b9 --- /dev/null +++ b/cli/load_balancer_enable_public_interface.go @@ -0,0 +1,44 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newLoadBalancerEnablePublicInterface(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "enable-public-interface [FLAGS] LOADBALANCER", + Short: "Enable the public interface of a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerEnablePublicInterface), + } + + return cmd +} + +func runLoadBalancerEnablePublicInterface(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + action, _, err := cli.Client().LoadBalancer.EnablePublicInterface(cli.Context, loadBalancer) + if err != nil { + return err + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + + fmt.Printf("Public interface of Load Balancer %d was enabled\n", loadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_list.go b/cli/load_balancer_list.go new file mode 100644 index 00000000..4c609bc8 --- /dev/null +++ b/cli/load_balancer_list.go @@ -0,0 +1,186 @@ +package cli + +import ( + "github.com/hetznercloud/hcloud-go/hcloud/schema" + "strings" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +var loadBalancerListTableOutput *tableOutput + +func init() { + loadBalancerListTableOutput = describeLoadBalancerListTableOutput(nil) +} + +func newLoadBalancerListCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "list [FLAGS]", + Short: "List Load Balancers", + Long: listLongDescription( + "Displays a list of Load Balancers.", + loadBalancerListTableOutput.Columns(), + ), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerList), + } + addOutputFlag(cmd, outputOptionNoHeader(), outputOptionColumns(loadBalancerListTableOutput.Columns()), outputOptionJSON()) + cmd.Flags().StringP("selector", "l", "", "Selector to filter by labels") + return cmd +} + +func runLoadBalancerList(cli *CLI, cmd *cobra.Command, args []string) error { + outOpts := outputFlagsForCommand(cmd) + + labelSelector, _ := cmd.Flags().GetString("selector") + opts := hcloud.LoadBalancerListOpts{ + ListOpts: hcloud.ListOpts{ + LabelSelector: labelSelector, + PerPage: 50, + }, + } + loadBalancers, err := cli.Client().LoadBalancer.AllWithOpts(cli.Context, opts) + if err != nil { + return err + } + if outOpts.IsSet("json") { + var loadBalancerSchemas []schema.LoadBalancer + for _, loadBalancer := range loadBalancers { + loadBalancerSchema := schema.LoadBalancer{ + ID: loadBalancer.ID, + Name: loadBalancer.Name, + PublicNet: schema.LoadBalancerPublicNet{ + Enabled: loadBalancer.PublicNet.Enabled, + IPv4: schema.LoadBalancerPublicNetIPv4{ + IP: loadBalancer.PublicNet.IPv4.IP.String(), + }, + IPv6: schema.LoadBalancerPublicNetIPv6{ + IP: loadBalancer.PublicNet.IPv6.IP.String(), + }, + }, + Created: loadBalancer.Created, + Labels: loadBalancer.Labels, + LoadBalancerType: loadBalancerTypeToSchema(*loadBalancer.LoadBalancerType), + Location: locationToSchema(*loadBalancer.Location), + Protection: schema.LoadBalancerProtection{ + Delete: loadBalancer.Protection.Delete, + }, + Algorithm: schema.LoadBalancerAlgorithm{Type: string(loadBalancer.Algorithm.Type)}, + } + for _, service := range loadBalancer.Services { + serviceSchema := schema.LoadBalancerService{ + Protocol: string(service.Protocol), + ListenPort: service.ListenPort, + DestinationPort: service.DestinationPort, + Proxyprotocol: service.Proxyprotocol, + HealthCheck: &schema.LoadBalancerServiceHealthCheck{ + Protocol: string(service.HealthCheck.Protocol), + Port: service.HealthCheck.Port, + Interval: int(service.HealthCheck.Interval.Seconds()), + Timeout: int(service.HealthCheck.Timeout.Seconds()), + Retries: service.HealthCheck.Retries, + }, + } + if service.Protocol != hcloud.LoadBalancerServiceProtocolTCP { + serviceSchema.HTTP = &schema.LoadBalancerServiceHTTP{ + StickySessions: service.HTTP.StickySessions, + CookieName: service.HTTP.CookieName, + CookieLifetime: int(service.HTTP.CookieLifetime.Seconds()), + RedirectHTTP: service.HTTP.RedirectHTTP, + } + } + if service.HealthCheck.HTTP != nil { + serviceSchema.HealthCheck.HTTP = &schema.LoadBalancerServiceHealthCheckHTTP{ + Domain: service.HealthCheck.HTTP.Domain, + Path: service.HealthCheck.HTTP.Path, + StatusCodes: service.HealthCheck.HTTP.StatusCodes, + TLS: service.HealthCheck.HTTP.TLS, + Response: service.HealthCheck.HTTP.Response, + } + } + loadBalancerSchema.Services = append(loadBalancerSchema.Services, serviceSchema) + } + for _, target := range loadBalancer.Targets { + targetSchema := schema.LoadBalancerTarget{ + Type: string(target.Type), + UsePrivateIP: target.UsePrivateIP, + } + if target.Type == hcloud.LoadBalancerTargetTypeServer { + targetSchema.Server = &schema.LoadBalancerTargetServer{ID: target.Server.Server.ID} + } + for _, healthStatus := range target.HealthStatus { + targetSchema.HealthStatus = append(targetSchema.HealthStatus, schema.LoadBalancerTargetHealthStatus{ + ListenPort: healthStatus.ListenPort, + Status: string(healthStatus.Status), + }) + } + loadBalancerSchema.Targets = append(loadBalancerSchema.Targets, targetSchema) + } + + loadBalancerSchemas = append(loadBalancerSchemas, loadBalancerSchema) + } + return describeJSON(loadBalancerSchemas) + } + cols := []string{"id", "name", "ipv4", "ipv6", "type", "location", "network_zone"} + if outOpts.IsSet("columns") { + cols = outOpts["columns"] + } + + tw := describeLoadBalancerListTableOutput(cli) + if err = tw.ValidateColumns(cols); err != nil { + return err + } + + if !outOpts.IsSet("noheader") { + tw.WriteHeader(cols) + } + for _, loadBalancer := range loadBalancers { + tw.Write(cols, loadBalancer) + } + tw.Flush() + return nil +} + +func describeLoadBalancerListTableOutput(cli *CLI) *tableOutput { + return newTableOutput(). + AddAllowedFields(hcloud.LoadBalancer{}). + AddFieldOutputFn("ipv4", fieldOutputFn(func(obj interface{}) string { + loadbalancer := obj.(*hcloud.LoadBalancer) + return loadbalancer.PublicNet.IPv4.IP.String() + })). + AddFieldOutputFn("ipv6", fieldOutputFn(func(obj interface{}) string { + loadbalancer := obj.(*hcloud.LoadBalancer) + return loadbalancer.PublicNet.IPv6.IP.String() + })). + AddFieldOutputFn("type", fieldOutputFn(func(obj interface{}) string { + loadbalancer := obj.(*hcloud.LoadBalancer) + return loadbalancer.LoadBalancerType.Name + })). + AddFieldOutputFn("location", fieldOutputFn(func(obj interface{}) string { + loadbalancer := obj.(*hcloud.LoadBalancer) + return loadbalancer.Location.Name + })). + AddFieldOutputFn("network_zone", fieldOutputFn(func(obj interface{}) string { + loadbalancer := obj.(*hcloud.LoadBalancer) + return string(loadbalancer.Location.NetworkZone) + })). + AddFieldOutputFn("labels", fieldOutputFn(func(obj interface{}) string { + loadBalancer := obj.(*hcloud.LoadBalancer) + return labelsToString(loadBalancer.Labels) + })). + AddFieldOutputFn("protection", fieldOutputFn(func(obj interface{}) string { + loadBalancer := obj.(*hcloud.LoadBalancer) + var protection []string + if loadBalancer.Protection.Delete { + protection = append(protection, "delete") + } + return strings.Join(protection, ", ") + })). + AddFieldOutputFn("created", fieldOutputFn(func(obj interface{}) string { + loadBalancer := obj.(*hcloud.LoadBalancer) + return datetime(loadBalancer.Created) + })) +} diff --git a/cli/load_balancer_remove_label.go b/cli/load_balancer_remove_label.go new file mode 100644 index 00000000..1ae105f5 --- /dev/null +++ b/cli/load_balancer_remove_label.go @@ -0,0 +1,76 @@ +package cli + +import ( + "errors" + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerRemoveLabelCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove-label [FLAGS] LOADBALANCER LABELKEY", + Short: "Remove a label from a Load Balancer", + Args: cobra.RangeArgs(1, 2), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: chainRunE(validateLoadBalancerRemoveLabel, cli.ensureToken), + RunE: cli.wrap(runLoadBalancerRemoveLabel), + } + + cmd.Flags().BoolP("all", "a", false, "Remove all labels") + return cmd +} + +func validateLoadBalancerRemoveLabel(cmd *cobra.Command, args []string) error { + all, _ := cmd.Flags().GetBool("all") + + if all && len(args) == 2 { + return errors.New("must not specify a label key when using --all/-a") + } + if !all && len(args) != 2 { + return errors.New("must specify a label key when not using --all/-a") + } + + return nil +} + +func runLoadBalancerRemoveLabel(cli *CLI, cmd *cobra.Command, args []string) error { + all, _ := cmd.Flags().GetBool("all") + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + labels := loadBalancer.Labels + if all { + labels = make(map[string]string) + } else { + label := args[1] + if _, ok := loadBalancer.Labels[label]; !ok { + return fmt.Errorf("label %s on Load Balancer %d does not exist", label, loadBalancer.ID) + } + delete(labels, label) + } + + opts := hcloud.LoadBalancerUpdateOpts{ + Labels: labels, + } + _, _, err = cli.Client().LoadBalancer.Update(cli.Context, loadBalancer, opts) + if err != nil { + return err + } + + if all { + fmt.Printf("All labels removed from Load Balancer %d\n", loadBalancer.ID) + } else { + fmt.Printf("Label %s removed from Load Balancer %d\n", args[1], loadBalancer.ID) + } + + return nil +} diff --git a/cli/load_balancer_remove_target.go b/cli/load_balancer_remove_target.go new file mode 100644 index 00000000..82f92779 --- /dev/null +++ b/cli/load_balancer_remove_target.go @@ -0,0 +1,63 @@ +package cli + +import ( + "fmt" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerRemoveTargetCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove-target LOADBALANCER FLAGS", + Short: "Remove a target to a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerRemoveTarget), + } + + cmd.Flags().String("server", "", "Name or ID of the server") + cmd.Flag("server").Annotations = map[string][]string{ + cobra.BashCompCustom: {"__hcloud_server_names"}, + } + + return cmd +} + +func runLoadBalancerRemoveTarget(cli *CLI, cmd *cobra.Command, args []string) error { + serverIdOrName, _ := cmd.Flags().GetString("server") + idOrName := args[0] + + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + var action *hcloud.Action + if serverIdOrName == "" { + return fmt.Errorf("specify a server") + } else if serverIdOrName != "" { + server, _, err := cli.Client().Server.Get(cli.Context, serverIdOrName) + if err != nil { + return err + } + if server == nil { + return fmt.Errorf("server not found: %s", serverIdOrName) + } + action, _, err = cli.Client().LoadBalancer.RemoveServerTarget(cli.Context, loadBalancer, server) + if err != nil { + return err + } + } + + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + fmt.Printf("Target removed from Load Balancer %d\n", loadBalancer.ID) + + return nil +} diff --git a/cli/load_balancer_type.go b/cli/load_balancer_type.go new file mode 100644 index 00000000..afb40bc9 --- /dev/null +++ b/cli/load_balancer_type.go @@ -0,0 +1,23 @@ +package cli + +import "github.com/spf13/cobra" + +func newLoadBalancerTypeCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "load-balancer-type", + Short: "Manage Load Balancer types", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableFlagsInUseLine: true, + RunE: cli.wrap(runLoadBalancerType), + } + cmd.AddCommand( + newLoadBalancerTypenDescribeCommand(cli), + newLoadBalancerTypeListCommand(cli), + ) + return cmd +} + +func runLoadBalancerType(cli *CLI, cmd *cobra.Command, args []string) error { + return cmd.Usage() +} diff --git a/cli/load_balancer_type_describe.go b/cli/load_balancer_type_describe.go new file mode 100644 index 00000000..0fc2eff0 --- /dev/null +++ b/cli/load_balancer_type_describe.go @@ -0,0 +1,70 @@ +package cli + +import ( + "encoding/json" + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerTypenDescribeCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe [FLAGS] LOADBALANCERTYPE", + Short: "Describe a Load Balancer type", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerTypeDescribe), + } + addOutputFlag(cmd, outputOptionJSON(), outputOptionFormat()) + return cmd +} + +func runLoadBalancerTypeDescribe(cli *CLI, cmd *cobra.Command, args []string) error { + outputFlags := outputFlagsForCommand(cmd) + + idOrName := args[0] + loadBalancerType, resp, err := cli.Client().LoadBalancerType.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancerType == nil { + return fmt.Errorf("loadBalancerType not found: %s", idOrName) + } + + switch { + case outputFlags.IsSet("json"): + return loadBalancerTypeDescribeJSON(resp) + case outputFlags.IsSet("format"): + return describeFormat(loadBalancerType, outputFlags["format"][0]) + default: + return loadBalancerTypeDescribeText(cli, loadBalancerType) + } +} + +func loadBalancerTypeDescribeText(cli *CLI, loadBalancerType *hcloud.LoadBalancerType) error { + fmt.Printf("ID:\t\t\t\t%d\n", loadBalancerType.ID) + fmt.Printf("Name:\t\t\t\t%s\n", loadBalancerType.Name) + fmt.Printf("Description:\t\t\t%s\n", loadBalancerType.Description) + fmt.Printf("Max Services:\t\t\t%d\n", loadBalancerType.MaxServices) + fmt.Printf("Max Connections:\t\t%d\n", loadBalancerType.MaxConnections) + fmt.Printf("Max Targets:\t\t\t%d\n", loadBalancerType.MaxTargets) + fmt.Printf("Max assigned Certificates:\t%d\n", loadBalancerType.MaxAssignedCertificates) + return nil +} + +func loadBalancerTypeDescribeJSON(resp *hcloud.Response) error { + var data map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + if loadBalancerType, ok := data["loadBalancerType"]; ok { + return describeJSON(loadBalancerType) + } + if loadBalancerTypes, ok := data["loadBalancerTypes"].([]interface{}); ok { + return describeJSON(loadBalancerTypes[0]) + } + return describeJSON(data) +} diff --git a/cli/load_balancer_type_list.go b/cli/load_balancer_type_list.go new file mode 100644 index 00000000..2dc89c7e --- /dev/null +++ b/cli/load_balancer_type_list.go @@ -0,0 +1,68 @@ +package cli + +import ( + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/hcloud-go/hcloud/schema" + "github.com/spf13/cobra" +) + +var loadBalancerTypeListTableOutput *tableOutput + +func init() { + loadBalancerTypeListTableOutput = newTableOutput(). + AddAllowedFields(hcloud.LoadBalancerType{}) +} + +func newLoadBalancerTypeListCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "list [FLAGS]", + Short: "List Load Balancer types", + Long: listLongDescription( + "Displays a list of Load Balancer types.", + loadBalancerTypeListTableOutput.Columns(), + ), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerTypeList), + } + addOutputFlag(cmd, outputOptionNoHeader(), outputOptionColumns(loadBalancerTypeListTableOutput.Columns()), outputOptionJSON()) + return cmd +} + +func runLoadBalancerTypeList(cli *CLI, cmd *cobra.Command, args []string) error { + outOpts := outputFlagsForCommand(cmd) + + loadBalancerTypes, err := cli.Client().LoadBalancerType.All(cli.Context) + if err != nil { + return err + } + + if outOpts.IsSet("json") { + var loadBalancerTypeSchemas []schema.LoadBalancerType + for _, loadBalancerType := range loadBalancerTypes { + loadBalancerTypeSchemas = append(loadBalancerTypeSchemas, loadBalancerTypeToSchema(*loadBalancerType)) + } + return describeJSON(loadBalancerTypeSchemas) + } + + cols := []string{"id", "name", "description", "max_services", "max_connections", "max_targets"} + if outOpts.IsSet("columns") { + cols = outOpts["columns"] + } + + tw := loadBalancerTypeListTableOutput + if err = tw.ValidateColumns(cols); err != nil { + return err + } + + if !outOpts.IsSet("noheader") { + tw.WriteHeader(cols) + } + for _, loadBalancerType := range loadBalancerTypes { + tw.Write(cols, loadBalancerType) + } + tw.Flush() + + return nil +} diff --git a/cli/load_balancer_update.go b/cli/load_balancer_update.go new file mode 100644 index 00000000..7600d450 --- /dev/null +++ b/cli/load_balancer_update.go @@ -0,0 +1,51 @@ +package cli + +import ( + "errors" + "fmt" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerUpdateCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "update [FLAGS] LOADBALANCER", + Short: "Update a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerUpdate), + } + + cmd.Flags().String("name", "", "Load Balancer name") + + return cmd +} + +func runLoadBalancerUpdate(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + + name, _ := cmd.Flags().GetString("name") + opts := hcloud.LoadBalancerUpdateOpts{ + Name: name, + } + if opts.Name == "" { + return errors.New("no updates") + } + + _, _, err = cli.Client().LoadBalancer.Update(cli.Context, loadBalancer, opts) + if err != nil { + return err + } + fmt.Printf("Load Balancer %d updated\n", loadBalancer.ID) + return nil +} diff --git a/cli/load_balancer_update_service.go b/cli/load_balancer_update_service.go new file mode 100644 index 00000000..5b4f0f3f --- /dev/null +++ b/cli/load_balancer_update_service.go @@ -0,0 +1,168 @@ +package cli + +import ( + "fmt" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/spf13/cobra" +) + +func newLoadBalancerUpdateServiceCommand(cli *CLI) *cobra.Command { + cmd := &cobra.Command{ + Use: "update-service LOADBALANCER FLAGS", + Short: "Updates a service from a Load Balancer", + Args: cobra.ExactArgs(1), + TraverseChildren: true, + DisableFlagsInUseLine: true, + PreRunE: cli.ensureToken, + RunE: cli.wrap(runLoadBalancerUpdateService), + } + + cmd.Flags().Int("listen-port", 0, "The listen port of the service that you want to update") + cmd.MarkFlagRequired("listen-port") + + cmd.Flags().Int("destination-port", 0, "Destination port of the service on the targets") + + cmd.Flags().String("protocol", "", "The protocol the health check is performed over") + cmd.Flags().Bool("proxy-protocol", false, "Enable or disable (with --proxy-protocol=false) Proxy Protocol") + cmd.Flags().Bool("http-redirect-http", false, "Enable or disable redirect all traffic on port 80 to port 443") + + cmd.Flags().Bool("http-sticky-sessions", false, "Enable or disable (with --http-sticky-sessions=false) Sticky Sessions") + cmd.Flags().String("http-cookie-name", "", "Sticky Sessions: Cookie Name which will be set") + cmd.Flags().Duration("http-cookie-lifetime", 0, "Sticky Sessions: Lifetime of the cookie") + cmd.Flags().IntSlice("http-certificates", []int{}, "ID of Certificates which are attached to this Load Balancer") + + cmd.Flags().String("health-check-protocol", "", "The protocol the health check is performed over") + cmd.Flags().Int("health-check-port", 0, "The port the health check is performed over") + cmd.Flags().Duration("health-check-interval", 15*time.Second, "The interval the health check is performed") + cmd.Flags().Duration("health-check-timeout", 10*time.Second, "The timeout after a health check is marked as failed") + cmd.Flags().Int("health-check-retries", 3, "Number of retries after a health check is marked as failed") + + cmd.Flags().String("health-check-http-domain", "", "The domain we request when performing a http health check") + cmd.Flags().String("health-check-http-path", "", "The path we request when performing a http health check") + + cmd.Flags().StringSlice("health-check-http-status-codes", []string{}, "List of status codes we expect to determine a target as healthy") + cmd.Flags().String("health-check-http-response", "", "The response we expect to determine a target as healthy") + cmd.Flags().Bool("health-check-http-tls", false, "Determine if the health check should verify if the target answers with a valid TLS certificate") + return cmd +} + +func runLoadBalancerUpdateService(cli *CLI, cmd *cobra.Command, args []string) error { + idOrName := args[0] + listenPort, _ := cmd.Flags().GetInt("listen-port") + + loadBalancer, _, err := cli.Client().LoadBalancer.Get(cli.Context, idOrName) + if err != nil { + return err + } + if loadBalancer == nil { + return fmt.Errorf("Load Balancer not found: %s", idOrName) + } + var service hcloud.LoadBalancerService + for _, _service := range loadBalancer.Services { + if _service.ListenPort == listenPort { + if _service.HealthCheck.HTTP != nil { + service = _service + } + } + } + opts := hcloud.LoadBalancerUpdateServiceOpts{ + HTTP: &hcloud.LoadBalancerUpdateServiceOptsHTTP{}, + HealthCheck: &hcloud.LoadBalancerUpdateServiceOptsHealthCheck{}, + } + if cmd.Flag("protocol").Changed { + protocol, _ := cmd.Flags().GetString("protocol") + opts.Protocol = hcloud.LoadBalancerServiceProtocol(protocol) + } + if cmd.Flag("destination-port").Changed { + destinationPort, _ := cmd.Flags().GetInt("destination-port") + opts.DestinationPort = &destinationPort + } + if cmd.Flag("proxy-protocol").Changed { + proxyProtocol, _ := cmd.Flags().GetBool("proxy-protocol") + opts.Proxyprotocol = hcloud.Bool(proxyProtocol) + } + // HTTP + if cmd.Flag("http-redirect-http").Changed { + redirectHTTP, _ := cmd.Flags().GetBool("http-redirect-http") + opts.HTTP.RedirectHTTP = hcloud.Bool(redirectHTTP) + } + if cmd.Flag("http-sticky-sessions").Changed { + stickySessions, _ := cmd.Flags().GetBool("http-sticky-sessions") + opts.HTTP.StickySessions = hcloud.Bool(stickySessions) + } + if cmd.Flag("http-cookie-name").Changed { + cookieName, _ := cmd.Flags().GetString("http-cookie-name") + opts.HTTP.CookieName = hcloud.String(cookieName) + } + if cmd.Flag("http-cookie-lifetime").Changed { + cookieLifetime, _ := cmd.Flags().GetDuration("http-cookie-lifetime") + opts.HTTP.CookieLifetime = hcloud.Duration(cookieLifetime) + } + if cmd.Flag("http-certificates").Changed { + certificates, _ := cmd.Flags().GetIntSlice("http-certificates") + for _, certificateID := range certificates { + opts.HTTP.Certificates = append(opts.HTTP.Certificates, &hcloud.Certificate{ID: certificateID}) + } + } + // Health Check + if cmd.Flag("health-check-protocol").Changed { + healthCheckProtocol, _ := cmd.Flags().GetString("health-check-protocol") + opts.HealthCheck.Protocol = hcloud.LoadBalancerServiceProtocol(healthCheckProtocol) + } + if cmd.Flag("health-check-port").Changed { + healthCheckPort, _ := cmd.Flags().GetInt("health-check-port") + opts.HealthCheck.Port = hcloud.Int(healthCheckPort) + } + if cmd.Flag("health-check-interval").Changed { + healthCheckInterval, _ := cmd.Flags().GetDuration("health-check-interval") + opts.HealthCheck.Interval = hcloud.Duration(healthCheckInterval) + } + if cmd.Flag("health-check-timeout").Changed { + healthCheckTimeout, _ := cmd.Flags().GetDuration("health-check-timeout") + opts.HealthCheck.Timeout = hcloud.Duration(healthCheckTimeout) + } + if cmd.Flag("health-check-retries").Changed { + healthCheckRetries, _ := cmd.Flags().GetInt("health-check-retries") + opts.HealthCheck.Retries = hcloud.Int(healthCheckRetries) + } + + // Health Check HTTP + healthCheckProtocol, _ := cmd.Flags().GetString("health-check-protocol") + if healthCheckProtocol != string(hcloud.LoadBalancerServiceProtocolTCP) || service.HealthCheck.Protocol != hcloud.LoadBalancerServiceProtocolTCP { + opts.HealthCheck.HTTP = &hcloud.LoadBalancerUpdateServiceOptsHealthCheckHTTP{} + + if cmd.Flag("health-check-http-domain").Changed { + healthCheckHTTPDomain, _ := cmd.Flags().GetString("health-check-http-domain") + opts.HealthCheck.HTTP.Domain = hcloud.String(healthCheckHTTPDomain) + } + if cmd.Flag("health-check-http-path").Changed { + healthCheckHTTPPath, _ := cmd.Flags().GetString("health-check-http-path") + opts.HealthCheck.HTTP.Path = hcloud.String(healthCheckHTTPPath) + } + if cmd.Flag("health-check-http-response").Changed { + healthCheckHTTPResponse, _ := cmd.Flags().GetString("health-check-http-response") + opts.HealthCheck.HTTP.Response = hcloud.String(healthCheckHTTPResponse) + } + if cmd.Flag("health-check-http-status-codes").Changed { + healthCheckHTTPStatusCodes, _ := cmd.Flags().GetStringSlice("health-check-http-status-codes") + opts.HealthCheck.HTTP.StatusCodes = healthCheckHTTPStatusCodes + } + if cmd.Flag("health-check-http-tls").Changed { + healthCheckHTTPTLS, _ := cmd.Flags().GetBool("health-check-http-tls") + opts.HealthCheck.HTTP.TLS = hcloud.Bool(healthCheckHTTPTLS) + } + } + + action, _, err := cli.Client().LoadBalancer.UpdateService(cli.Context, loadBalancer, listenPort, opts) + if err != nil { + return err + } + if err := cli.ActionProgress(cli.Context, action); err != nil { + return err + } + fmt.Printf("Service %d on Load Balancer %d was updated\n", listenPort, loadBalancer.ID) + + return nil +} diff --git a/cli/root.go b/cli/root.go index 97f1068a..6fd32e59 100644 --- a/cli/root.go +++ b/cli/root.go @@ -32,6 +32,9 @@ func NewRootCommand(cli *CLI) *cobra.Command { newISOCommand(cli), newVolumeCommand(cli), newNetworkCommand(cli), + newLoadBalancerCommand(cli), + newLoadBalancerTypeCommand(cli), + newCertificatesCommand(cli), ) cmd.PersistentFlags().Duration("poll-interval", 500*time.Millisecond, "Interval at which to poll information, for example action progress") return cmd diff --git a/cli/server_create.go b/cli/server_create.go index 2516bcda..f30f4258 100644 --- a/cli/server_create.go +++ b/cli/server_create.go @@ -60,7 +60,7 @@ func newServerCreateCommand(cli *CLI) *cobra.Command { cmd.Flags().StringArray("user-data-from-file", []string{}, "Read user data from specified file (use - to read from stdin)") - cmd.Flags().Bool("start-after-create", true, "Start server right after creation (default: true)") + cmd.Flags().Bool("start-after-create", true, "Start server right after creation") cmd.Flags().StringSlice("volume", nil, "ID or name of volume to attach (can be specified multiple times)") cmd.Flag("volume").Annotations = map[string][]string{ diff --git a/cli/util.go b/cli/util.go index e93e9a1b..c533911b 100644 --- a/cli/util.go +++ b/cli/util.go @@ -191,3 +191,29 @@ func isoToSchema(iso hcloud.ISO) schema.ISO { Deprecated: iso.Deprecated, } } + +func loadBalancerTypeToSchema(loadBalancerType hcloud.LoadBalancerType) schema.LoadBalancerType { + loadBalancerTypeSchema := schema.LoadBalancerType{ + ID: loadBalancerType.ID, + Name: loadBalancerType.Name, + Description: loadBalancerType.Description, + MaxConnections: loadBalancerType.MaxConnections, + MaxServices: loadBalancerType.MaxServices, + MaxTargets: loadBalancerType.MaxTargets, + MaxAssignedCertificates: loadBalancerType.MaxAssignedCertificates, + } + for _, pricing := range loadBalancerType.Pricings { + loadBalancerTypeSchema.Prices = append(loadBalancerTypeSchema.Prices, schema.PricingLoadBalancerTypePrice{ + Location: pricing.Location.Name, + PriceHourly: schema.Price{ + Net: pricing.Hourly.Net, + Gross: pricing.Hourly.Gross, + }, + PriceMonthly: schema.Price{ + Net: pricing.Monthly.Net, + Gross: pricing.Monthly.Gross, + }, + }) + } + return loadBalancerTypeSchema +} diff --git a/go.mod b/go.mod index 431a024d..feab1356 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ require ( github.com/cheggaaa/pb/v3 v3.0.4 github.com/dustin/go-humanize v1.0.0 github.com/fatih/structs v1.1.0 - github.com/hetznercloud/hcloud-go v1.17.0 + github.com/hetznercloud/hcloud-go v1.18.0 github.com/pelletier/go-toml v1.7.0 github.com/spf13/cobra v0.0.7 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index b97a69d7..278bb86d 100644 --- a/go.sum +++ b/go.sum @@ -44,13 +44,15 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hetznercloud/hcloud-go v1.17.0 h1:IKH0GLLoTEfgMuBY+GaaVTwjYChecrHFVo4/t0sIkGU= -github.com/hetznercloud/hcloud-go v1.17.0/go.mod h1:8lR3yHBHZWy2uGcUi9Ibt4UOoop2wrVdERJgCtxsF3Q= +github.com/hetznercloud/hcloud-go v1.18.0 h1:gNmwDQ/Jt7bc7dqb0E1x5hJ52yyYJN8q8OHh/Oq3mMo= +github.com/hetznercloud/hcloud-go v1.18.0/go.mod h1:EhElojlVU1biA5JgBaV8rRU1vE5+iYke402kXC9pooE= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -149,6 +151,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=