From 625a8baed33c3bdd6adcd6e6e18beeda7e2ec99c Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Sat, 15 Feb 2025 10:51:02 +1000 Subject: [PATCH] feat: implement 'caddy deploy' command to deploy global service using caddy:latest image --- cmd/uncloud/caddy/deploy.go | 23 +++++++++++-- internal/cli/cli.go | 7 ++++ internal/cli/client/caddy.go | 41 +++++++++++++++++++++--- internal/cli/client/deploy.go | 2 +- internal/cli/client/strategy.go | 4 +-- test/e2e/service_test.go | 57 ++++++++++++++++++++++++++++----- 6 files changed, 115 insertions(+), 19 deletions(-) diff --git a/cmd/uncloud/caddy/deploy.go b/cmd/uncloud/caddy/deploy.go index 2daf7ce9..288a3810 100644 --- a/cmd/uncloud/caddy/deploy.go +++ b/cmd/uncloud/caddy/deploy.go @@ -3,6 +3,7 @@ package caddy import ( "context" "fmt" + "github.com/docker/compose/v2/pkg/progress" "github.com/spf13/cobra" "uncloud/internal/cli" ) @@ -43,9 +44,25 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { } defer client.Close() - if err = client.DeployCaddy(ctx, opts.image); err != nil { - return fmt.Errorf("deploy caddy: %w", err) + d, err := client.NewCaddyDeployment(opts.image) + if err != nil { + return fmt.Errorf("create caddy deployment: %w", err) } - return nil + plan, err := d.Plan(ctx) + if err != nil { + return fmt.Errorf("plan caddy deployment: %w", err) + } + + if len(plan.SequenceOperation.Operations) == 0 { + fmt.Println("caddy service is up to date.") + return nil + } + + return progress.RunWithTitle(ctx, func(ctx context.Context) error { + if _, err = d.Run(ctx); err != nil { + return fmt.Errorf("deploy caddy: %w", err) + } + return nil + }, uncli.ProgressOut(), "Deploying service "+d.Spec.Name) } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 2b535271..3a95249e 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -4,7 +4,9 @@ import ( "context" "errors" "fmt" + "github.com/docker/cli/cli/streams" "net/netip" + "os" "uncloud/internal/cli/client" "uncloud/internal/cli/client/connector" "uncloud/internal/cli/config" @@ -328,3 +330,8 @@ func (cli *CLI) promptResetMachine() error { // TODO: implement resetting the remote machine. return fmt.Errorf("resetting the remote machine is not implemented yet") } + +// ProgressOut returns an output stream for progress writer. +func (cli *CLI) ProgressOut() *streams.Out { + return streams.NewOut(os.Stdout) +} diff --git a/internal/cli/client/caddy.go b/internal/cli/client/caddy.go index 27643ad3..c3c02ca2 100644 --- a/internal/cli/client/caddy.go +++ b/internal/cli/client/caddy.go @@ -1,10 +1,43 @@ package client import ( - "context" - "errors" + "uncloud/internal/api" ) -func (cli *Client) DeployCaddy(ctx context.Context, image string) error { - return errors.New("not implemented") +const CaddyServiceName = "caddy" + +// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service. +// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest +// version of the official Caddy Docker image is used. +func (cli *Client) NewCaddyDeployment(image string) (*Deployment, error) { + if image == "" { + // TODO: fetch the latest version tag from the Docker Hub registry. + image = "caddy:latest" + } + + spec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"}, + Image: image, + Volumes: []string{"/var/lib/uncloud/caddy:/config"}, + }, + Mode: api.ServiceModeGlobal, + Name: CaddyServiceName, + Ports: []api.PortSpec{ + { + PublishedPort: 80, + ContainerPort: 80, + Protocol: api.ProtocolTCP, + Mode: api.PortModeHost, + }, + { + PublishedPort: 443, + ContainerPort: 443, + Protocol: api.ProtocolTCP, + Mode: api.PortModeHost, + }, + }, + } + + return cli.NewDeployment(spec, &RollingStrategy{}) } diff --git a/internal/cli/client/deploy.go b/internal/cli/client/deploy.go index 5668295c..1dc23341 100644 --- a/internal/cli/client/deploy.go +++ b/internal/cli/client/deploy.go @@ -22,7 +22,7 @@ type Deployment struct { type Plan struct { ServiceID string - Operation + SequenceOperation } // NewDeployment creates a new deployment for the given service specification. diff --git a/internal/cli/client/strategy.go b/internal/cli/client/strategy.go index b7c740b7..c1542480 100644 --- a/internal/cli/client/strategy.go +++ b/internal/cli/client/strategy.go @@ -73,7 +73,6 @@ func (s *RollingStrategy) planGlobal( return plan, fmt.Errorf("list machines: %w", err) } - seqOp := &SequenceOperation{} // TODO: figure out how to return a warning if there are machines down. Embed the machinesDown in the plan? // WARNING: failed to run a service container on machine '%s' which is Down. var machinesDown []*pb.MachineInfo @@ -89,9 +88,8 @@ func (s *RollingStrategy) planGlobal( if err != nil { return plan, err } - seqOp.Operations = append(seqOp.Operations, ops...) + plan.SequenceOperation.Operations = append(plan.SequenceOperation.Operations, ops...) } - plan.Operation = seqOp return plan, nil } diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go index e85b1a33..59b0d918 100644 --- a/test/e2e/service_test.go +++ b/test/e2e/service_test.go @@ -51,8 +51,7 @@ func TestDeployment(t *testing.T) { plan, err := deploy.Plan(ctx) require.NoError(t, err) - assert.IsType(t, &client.SequenceOperation{}, plan.Operation) - assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 3) // 3 run + assert.Len(t, plan.SequenceOperation.Operations, 3) // 3 run svcID, err := deploy.Run(ctx) require.NoError(t, err) @@ -89,8 +88,7 @@ func TestDeployment(t *testing.T) { plan, err = deploy.Plan(ctx) require.NoError(t, err) - assert.IsType(t, &client.SequenceOperation{}, plan.Operation) - assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 6) // 3 run + 3 remove + assert.Len(t, plan.SequenceOperation.Operations, 6) // 3 run + 3 remove svcID, err = deploy.Run(ctx) require.NoError(t, err) @@ -129,8 +127,7 @@ func TestDeployment(t *testing.T) { plan, err = deploy.Plan(ctx) require.NoError(t, err) - assert.IsType(t, &client.SequenceOperation{}, plan.Operation) - assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 9) // 3 stop + 3 run + 3 remove + assert.Len(t, plan.SequenceOperation.Operations, 9) // 3 stop + 3 run + 3 remove svcID, err = deploy.Run(ctx) require.NoError(t, err) @@ -152,8 +149,7 @@ func TestDeployment(t *testing.T) { plan, err = deploy.Plan(ctx) require.NoError(t, err) - assert.IsType(t, &client.SequenceOperation{}, plan.Operation) - assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 0) // no-op + assert.Len(t, plan.SequenceOperation.Operations, 0) // no-op svcID, err = deploy.Run(ctx) require.NoError(t, err) @@ -165,6 +161,51 @@ func TestDeployment(t *testing.T) { assert.Equal(t, api.ServiceModeGlobal, svc.Mode) assert.Len(t, svc.Containers, 3) }) + + t.Run("caddy", func(t *testing.T) { + t.Parallel() + + name := "caddy" + t.Cleanup(func() { + err := cli.RemoveService(ctx, name) + if errors.Is(err, client.ErrNotFound) { + require.NoError(t, err) + } + }) + + deploy, err := cli.NewCaddyDeployment("") + require.NoError(t, err) + + _, err = deploy.Run(ctx) + require.NoError(t, err) + + svc, err := cli.InspectService(ctx, name) + require.NoError(t, err) + assert.Equal(t, name, svc.Name) + assert.Equal(t, api.ServiceModeGlobal, svc.Mode) + assert.Len(t, svc.Containers, 3) + + ctr := svc.Containers[0].Container + assert.Equal(t, "caddy:latest", ctr.Config.Image) + + ports, err := ctr.ServicePorts() + require.NoError(t, err) + expectedPorts := []api.PortSpec{ + { + PublishedPort: 80, + ContainerPort: 80, + Protocol: api.ProtocolTCP, + Mode: api.PortModeHost, + }, + { + PublishedPort: 443, + ContainerPort: 443, + Protocol: api.ProtocolTCP, + Mode: api.PortModeHost, + }, + } + assert.Equal(t, expectedPorts, ports) + }) } func TestRunService(t *testing.T) {