diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 561a67bb..7f2a7280 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -263,6 +263,11 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i vmUpdateCmd := runCmds.InitVMUpdateCommand(vmCmd) runCmds.InitVMUpdateTTL(vmUpdateCmd) + vmPortCmd := runCmds.InitVMPort(vmCmd) + runCmds.InitVMPortLs(vmPortCmd) + runCmds.InitVMPortExpose(vmPortCmd) + runCmds.InitVMPortRm(vmPortCmd) + networkCmd := runCmds.InitNetworkCommand(runCmds.rootCmd) runCmds.InitNetworkCreate(networkCmd) runCmds.InitNetworkList(networkCmd) diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index af2efbc5..ac964aa3 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -238,4 +238,9 @@ type runnerArgs struct { updateVMName string updateVMID string + + vmExposePortPort int + vmExposePortProtocols []string + vmExposePortIsWildcard bool + vmPortRemoveAddonID string } diff --git a/cli/cmd/vm_port.go b/cli/cmd/vm_port.go new file mode 100644 index 00000000..3a0e0f5b --- /dev/null +++ b/cli/cmd/vm_port.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func (r *runners) InitVMPort(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "port", + Short: "Manage VM ports.", + Long: `The 'vm port' command is a parent command for managing ports in a vm. It allows users to list, remove, or expose specific ports used by the vm. Use the subcommands (such as 'ls', 'rm', and 'expose') to manage port configurations effectively. + +This command provides flexibility for handling ports in various test vms, ensuring efficient management of vm networking settings.`, + Example: ` # List all exposed ports in a vm + replicated vm port ls [VM_ID] + + # Remove an exposed port from a vm + replicated vm port rm [VM_ID] [PORT] + + # Expose a new port in a vm + replicated vm port expose [VM_ID] [PORT]`, + SilenceUsage: true, + Hidden: false, + } + parent.AddCommand(cmd) + + return cmd +} diff --git a/cli/cmd/vm_port_expose.go b/cli/cmd/vm_port_expose.go new file mode 100644 index 00000000..34c7f604 --- /dev/null +++ b/cli/cmd/vm_port_expose.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/cli/print" + "github.com/spf13/cobra" +) + +func (r *runners) InitVMPortExpose(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "expose VM_ID --port PORT", + Short: "Expose a port on a vm to the public internet.", + Long: `The 'vm port expose' command is used to expose a specified port on a vm to the public internet. When exposing a port, the command automatically creates a DNS entry and, if using the "https" protocol, provisions a TLS certificate for secure communication. + +You can also create a wildcard DNS entry and TLS certificate by specifying the "--wildcard" flag. Please note that creating a wildcard certificate may take additional time. + +This command supports different protocols including "http", "https", "ws", and "wss" for web traffic and web socket communication.`, + Example: ` # Expose port 8080 with HTTPS protocol and wildcard DNS + replicated vm port expose VM_ID --port 8080 --protocol https --wildcard + + # Expose port 3000 with HTTP protocol + replicated vm port expose VM_ID --port 3000 --protocol http + + # Expose port 8080 with multiple protocols + replicated vm port expose VM_ID --port 8080 --protocol http,https + + # Expose port 8080 and display the result in JSON format + replicated vm port expose VM_ID --port 8080 --protocol https --output json`, + RunE: r.vmPortExpose, + Args: cobra.ExactArgs(1), + ValidArgsFunction: r.completeVMIDs, + } + parent.AddCommand(cmd) + + cmd.Flags().IntVar(&r.args.vmExposePortPort, "port", 0, "Port to expose (required)") + err := cmd.MarkFlagRequired("port") + if err != nil { + panic(err) + } + cmd.Flags().StringSliceVar(&r.args.vmExposePortProtocols, "protocol", []string{"http", "https"}, `Protocol to expose (valid values are "http", "https", "ws" and "wss")`) + cmd.Flags().BoolVar(&r.args.vmExposePortIsWildcard, "wildcard", false, "Create a wildcard DNS entry and TLS certificate for this port") + cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table|wide (default: table)") + + return cmd +} + +func (r *runners) vmPortExpose(_ *cobra.Command, args []string) error { + vmID := args[0] + + if len(r.args.vmExposePortProtocols) == 0 { + return errors.New("at least one protocol must be specified") + } + + port, err := r.kotsAPI.ExposeVMPort( + vmID, + r.args.vmExposePortPort, r.args.vmExposePortProtocols, r.args.vmExposePortIsWildcard, + ) + if err != nil { + return err + } + + return print.VMPort(r.outputFormat, r.w, port) +} diff --git a/cli/cmd/vm_port_ls.go b/cli/cmd/vm_port_ls.go new file mode 100644 index 00000000..49c8f881 --- /dev/null +++ b/cli/cmd/vm_port_ls.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "github.com/replicatedhq/replicated/cli/print" + "github.com/spf13/cobra" +) + +func (r *runners) InitVMPortLs(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "ls VM_ID", + Short: "List vm ports for a vm.", + Long: `The 'vm port ls' command lists all the ports configured for a specific vm. You must provide the vm ID to retrieve and display the ports. + +This command is useful for viewing the current port configurations, protocols, and other related settings of your test vm. The output format can be customized to suit your needs, and the available formats include table, JSON, and wide views.`, + Example: ` # List ports for a vm in the default table format + replicated vm port ls VM_ID + + # List ports for a vm in JSON format + replicated vm port ls VM_ID --output json + + # List ports for a vm in wide format + replicated vm port ls VM_ID --output wide`, + RunE: r.vmPortList, + Args: cobra.ExactArgs(1), + ValidArgsFunction: r.completeVMIDs, + } + parent.AddCommand(cmd) + + cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table|wide (default: table)") + + return cmd +} + +func (r *runners) vmPortList(_ *cobra.Command, args []string) error { + vmID := args[0] + + ports, err := r.kotsAPI.ListVMPorts(vmID) + if err != nil { + return err + } + + return print.VMPorts(r.outputFormat, r.w, ports, true) +} diff --git a/cli/cmd/vm_port_rm.go b/cli/cmd/vm_port_rm.go new file mode 100644 index 00000000..f438f233 --- /dev/null +++ b/cli/cmd/vm_port_rm.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "github.com/replicatedhq/replicated/cli/print" + "github.com/spf13/cobra" +) + +func (r *runners) InitVMPortRm(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "rm VM_ID --id PORT_ID", + Short: "Remove vm port by ID.", + Long: `The 'vm port rm' command removes a specific port from a vm. You must provide the ID of the port to remove. + +This command is useful for managing the network settings of your test vms by allowing you to clean up unused or incorrect ports. After removing a port, the updated list of ports will be displayed.`, + Example: ` # Remove a port using its ID + replicated vm port rm VM_ID --id PORT_ID + + # Remove a port and display the result in JSON format + replicated vm port rm VM_ID --id PORT_ID --output json`, + RunE: r.vmPortRemove, + Args: cobra.ExactArgs(1), + ValidArgsFunction: r.completeVMIDs, + } + parent.AddCommand(cmd) + + cmd.Flags().StringVar(&r.args.vmPortRemoveAddonID, "id", "", "ID of the port to remove (required)") + err := cmd.MarkFlagRequired("id") + if err != nil { + panic(err) + } + cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table|wide (default: table)") + + return cmd +} + +func (r *runners) vmPortRemove(_ *cobra.Command, args []string) error { + vmID := args[0] + + err := r.kotsAPI.DeleteVMAddon(vmID, r.args.vmPortRemoveAddonID) + if err != nil { + return err + } + + ports, err := r.kotsAPI.ListVMPorts(vmID) + if err != nil { + return err + } + + return print.VMPorts(r.outputFormat, r.w, ports, true) +} diff --git a/cli/print/vm_ports.go b/cli/print/vm_ports.go new file mode 100644 index 00000000..ff1313aa --- /dev/null +++ b/cli/print/vm_ports.go @@ -0,0 +1,85 @@ +package print + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" + "text/template" + + "github.com/replicatedhq/replicated/pkg/types" +) + +var vmPortsTmplHeaderSrc = `ID VM PORT PROTOCOL EXPOSED PORT WILDCARD STATUS` +var vmPortsTmplRowSrc = `{{- range . }} +{{- $id := .AddonID }} +{{- $upstreamPort := .UpstreamPort }} +{{- $hostname := .Hostname }} +{{- $isWildcard := .IsWildcard }} +{{- $state := .State }} +{{- range .ExposedPorts }} +{{ $id }} {{ $upstreamPort }} {{ .Protocol }} {{ formatURL .Protocol $hostname }} {{ $isWildcard }} {{ printf "%-12s" $state }} +{{ end }} +{{ end }}` +var vmPortsTmplSrc = fmt.Sprintln(vmPortsTmplHeaderSrc) + vmPortsTmplRowSrc +var vmPortsTmpl = template.Must(template.New("ports").Funcs(funcs).Parse(vmPortsTmplSrc)) +var vmPortsTmplNoHeader = template.Must(template.New("ports").Funcs(funcs).Parse(vmPortsTmplRowSrc)) + +const ( + vmPortsMinWidth = 16 + vmPortsTabWidth = 8 + vmPortsPadding = 4 + vmPortsPadChar = ' ' +) + +func VMPorts(outputFormat string, w *tabwriter.Writer, ports []*types.VMPort, header bool) error { + // we need a custom tab writer here because our column widths are large + portsWriter := tabwriter.NewWriter(os.Stdout, vmPortsMinWidth, vmPortsTabWidth, vmPortsPadding, vmPortsPadChar, tabwriter.TabIndent) + + switch outputFormat { + case "table", "wide": + if header { + if err := vmPortsTmpl.Execute(portsWriter, ports); err != nil { + return err + } + } else { + if err := vmPortsTmplNoHeader.Execute(portsWriter, ports); err != nil { + return err + } + } + case "json": + cAsByte, err := json.MarshalIndent(ports, "", " ") + if err != nil { + return err + } + if _, err := fmt.Fprintln(portsWriter, string(cAsByte)); err != nil { + return err + } + default: + return fmt.Errorf("unsupported output format: %s", outputFormat) + } + return w.Flush() +} + +func VMPort(outputFormat string, w *tabwriter.Writer, port *types.VMPort) error { + // we need a custom tab writer here because our column widths are large + portsWriter := tabwriter.NewWriter(os.Stdout, vmPortsMinWidth, vmPortsTabWidth, vmPortsPadding, vmPortsPadChar, tabwriter.TabIndent) + + switch outputFormat { + case "table": + if err := vmPortsTmpl.Execute(portsWriter, []*types.VMPort{port}); err != nil { + return err + } + case "json": + cAsByte, err := json.MarshalIndent(port, "", " ") + if err != nil { + return err + } + if _, err := fmt.Fprintln(portsWriter, string(cAsByte)); err != nil { + return err + } + default: + return fmt.Errorf("unsupported output format: %s", outputFormat) + } + return w.Flush() +} diff --git a/pkg/kotsclient/vm_addon_rm.go b/pkg/kotsclient/vm_addon_rm.go new file mode 100644 index 00000000..c4fbc30e --- /dev/null +++ b/pkg/kotsclient/vm_addon_rm.go @@ -0,0 +1,17 @@ +package kotsclient + +import ( + "context" + "fmt" + "net/http" +) + +func (c *VendorV3Client) DeleteVMAddon(vmID, addonID string) error { + endpoint := fmt.Sprintf("/v3/vm/%s/addons/%s", vmID, addonID) + err := c.DoJSON(context.TODO(), "DELETE", endpoint, http.StatusNoContent, nil, nil) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/kotsclient/vm_port_expose.go b/pkg/kotsclient/vm_port_expose.go new file mode 100644 index 00000000..e9fa1e9c --- /dev/null +++ b/pkg/kotsclient/vm_port_expose.go @@ -0,0 +1,35 @@ +package kotsclient + +import ( + "context" + "fmt" + "net/http" + + "github.com/replicatedhq/replicated/pkg/types" +) + +type ExportVMPortRequest struct { + Port int `json:"port"` + Protocols []string `json:"protocols"` + IsWildcard bool `json:"is_wildcard"` +} + +type ExposeVMPortResponse struct { + Port *types.VMPort `json:"port"` +} + +func (c *VendorV3Client) ExposeVMPort(vmID string, portNumber int, protocols []string, isWildcard bool) (*types.VMPort, error) { + req := ExportVMPortRequest{ + Port: portNumber, + Protocols: protocols, + IsWildcard: isWildcard, + } + + resp := ExposeVMPortResponse{} + err := c.DoJSON(context.TODO(), "POST", fmt.Sprintf("/v3/vm/%s/port", vmID), http.StatusCreated, req, &resp) + if err != nil { + return nil, err + } + + return resp.Port, nil +} diff --git a/pkg/kotsclient/vm_port_list.go b/pkg/kotsclient/vm_port_list.go new file mode 100644 index 00000000..fb1e2e51 --- /dev/null +++ b/pkg/kotsclient/vm_port_list.go @@ -0,0 +1,23 @@ +package kotsclient + +import ( + "context" + "fmt" + "net/http" + + "github.com/replicatedhq/replicated/pkg/types" +) + +type ListVMPortsResponse struct { + Ports []*types.VMPort `json:"ports"` +} + +func (c *VendorV3Client) ListVMPorts(vmID string) ([]*types.VMPort, error) { + resp := ListVMPortsResponse{} + err := c.DoJSON(context.TODO(), "GET", fmt.Sprintf("/v3/vm/%s/ports", vmID), http.StatusOK, nil, &resp) + if err != nil { + return nil, err + } + + return resp.Ports, nil +} diff --git a/pkg/types/vm.go b/pkg/types/vm.go index 3830fe87..ea3ead86 100644 --- a/pkg/types/vm.go +++ b/pkg/types/vm.go @@ -45,3 +45,31 @@ type VMVersion struct { InstanceTypes []string `json:"instance_types"` Status *ClusterDistributionStatus `json:"status,omitempty"` } + +type VMAddonStatus string + +const ( + VMAddonStatusPending VMAddonStatus = "pending" // No attempts to install this addon + VMAddonStatusApplied VMAddonStatus = "applied" // The addon has been applied to the vm + VMAddonStatusRunning VMAddonStatus = "ready" // The addon is ready to be used + VMAddonStatusError VMAddonStatus = "error" // The addon has an error + VMAddonStatusRemoving VMAddonStatus = "removing" // The addon is being removed + VMAddonStatusRemoved VMAddonStatus = "removed" // The addon has been removed +) + +type VMExposedPort struct { + Protocol string `json:"protocol"` + ExposedPort int `json:"exposed_port"` +} + +type VMPort struct { + VMID string `json:"vm_id"` + AddonID string `json:"addon_id"` + UpstreamPort int `json:"upstream_port"` + ExposedPorts []VMExposedPort `json:"exposed_ports"` + IsWildcard bool `json:"is_wildcard"` + CreatedAt time.Time `json:"created_at"` + Hostname string `json:"hostname"` + PortName string `json:"port_name"` + State VMAddonStatus `json:"state"` +}