diff --git a/cmd/uncloud/image/push.go b/cmd/uncloud/image/push.go new file mode 100644 index 00000000..f277205a --- /dev/null +++ b/cmd/uncloud/image/push.go @@ -0,0 +1,47 @@ +package image + +import ( + "fmt" + + "github.com/psviderski/uncloud/internal/cli" + "github.com/spf13/cobra" +) + +type pushOptions struct { + machines []string +} + +func NewPushCommand() *cobra.Command { + opts := pushOptions{} + + cmd := &cobra.Command{ + Use: "push IMAGE", + Short: "Upload a local Docker image to the cluster.", + Long: `Upload a local Docker image to the cluster transferring only the missing layers. +The image is uploaded to the machine which CLI is connected to (default) or the specified machine(s).`, + Example: ` # Push image to the currently connected machine. + uc image push myapp:latest + + # Push image to specific machine. + uc image push myapp:latest -m machine1 + + # Push image to multiple machines. + uc image push myapp:latest -m machine1,machine2,machine3`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + image := args[0] + + machines := cli.ExpandCommaSeparatedValues(opts.machines) + + // TODO: Implement image push logic + fmt.Printf("Would push image %q to machines: %v\n", image, machines) + return fmt.Errorf("image push not yet implemented") + }, + } + + cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil, + "Machine names to push the image to. Can be specified multiple times or as a comma-separated "+ + "list of machine names. (default is connected machine)") + + return cmd +} diff --git a/cmd/uncloud/image/root.go b/cmd/uncloud/image/root.go new file mode 100644 index 00000000..fb3b6025 --- /dev/null +++ b/cmd/uncloud/image/root.go @@ -0,0 +1,18 @@ +package image + +import ( + "github.com/spf13/cobra" +) + +func NewRootCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "image", + Short: "Manage Docker images in a cluster.", + } + + cmd.AddCommand( + NewPushCommand(), + ) + + return cmd +} diff --git a/cmd/uncloud/main.go b/cmd/uncloud/main.go index 1eb723a1..3e17c8b3 100644 --- a/cmd/uncloud/main.go +++ b/cmd/uncloud/main.go @@ -9,6 +9,7 @@ import ( "github.com/psviderski/uncloud/cmd/uncloud/caddy" cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context" "github.com/psviderski/uncloud/cmd/uncloud/dns" + "github.com/psviderski/uncloud/cmd/uncloud/image" "github.com/psviderski/uncloud/cmd/uncloud/machine" "github.com/psviderski/uncloud/cmd/uncloud/service" "github.com/psviderski/uncloud/cmd/uncloud/volume" @@ -82,6 +83,7 @@ func main() { caddy.NewRootCommand(), cmdcontext.NewRootCommand(), dns.NewRootCommand(), + image.NewRootCommand(), machine.NewRootCommand(), service.NewRootCommand(), service.NewInspectCommand(), diff --git a/pkg/client/client.go b/pkg/client/client.go index 108de3d3..277707c3 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -10,6 +10,7 @@ import ( "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/pkg/api" + "golang.org/x/net/proxy" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -31,9 +32,12 @@ type Client struct { var _ api.Client = (*Client)(nil) -// Connector is an interface for establishing a connection to the machine API. +// Connector is an interface for establishing a connection to the cluster. type Connector interface { + // Connect establishes a gRPC client connection to the machine API. Connect(ctx context.Context) (*grpc.ClientConn, error) + // Dialer returns a proxy dialer for establishing connections within the cluster if supported by the connector. + Dialer() (proxy.ContextDialer, error) Close() error } diff --git a/pkg/client/connector/ssh.go b/pkg/client/connector/ssh.go index 1260ef88..38812022 100644 --- a/pkg/client/connector/ssh.go +++ b/pkg/client/connector/ssh.go @@ -9,6 +9,7 @@ import ( "github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/sshexec" "golang.org/x/crypto/ssh" + "golang.org/x/net/proxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -79,6 +80,10 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) { return conn, nil } +func (c *SSHConnector) Dialer() (proxy.ContextDialer, error) { + return nil, fmt.Errorf("dialer not implemented for SSHConnector yet") +} + func (c *SSHConnector) Close() error { if c.client != nil { err := c.client.Close() diff --git a/pkg/client/connector/tcp.go b/pkg/client/connector/tcp.go index 9072ecab..0db2a9d0 100644 --- a/pkg/client/connector/tcp.go +++ b/pkg/client/connector/tcp.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" + "golang.org/x/net/proxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -29,6 +30,10 @@ func (c *TCPConnector) Connect(_ context.Context) (*grpc.ClientConn, error) { return conn, nil } +func (c *TCPConnector) Dialer() (proxy.ContextDialer, error) { + return nil, fmt.Errorf("proxy connections are not supported over a TCP connection") +} + func (c *TCPConnector) Close() error { return nil } diff --git a/pkg/client/connector/wireguard.go b/pkg/client/connector/wireguard.go index 0a3ca496..2e1375c3 100644 --- a/pkg/client/connector/wireguard.go +++ b/pkg/client/connector/wireguard.go @@ -12,6 +12,7 @@ import ( "github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network/tunnel" "github.com/psviderski/uncloud/pkg/client" + "golang.org/x/net/proxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -75,6 +76,10 @@ func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, err return conn, nil } +func (c *WireGuardConnector) Dialer() (proxy.ContextDialer, error) { + return nil, fmt.Errorf("proxy connections not implemented for WireGuard connector") +} + func (c *WireGuardConnector) Close() error { if c.tun != nil { c.tun.Close()