feat: deploy caddy service when adding new machine to cluster

This commit is contained in:
Pavel Sviderski
2025-02-18 21:35:36 +10:00
parent c3b88077ac
commit bcb5a2a173
4 changed files with 122 additions and 39 deletions
+100 -1
View File
@@ -1,14 +1,25 @@
package machine package machine
import ( import (
"context"
"errors"
"fmt" "fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"time"
"uncloud/internal/cli" "uncloud/internal/cli"
"uncloud/internal/cli/client"
"uncloud/internal/cli/config" "uncloud/internal/cli/config"
"uncloud/internal/machine/api/pb"
) )
type addOptions struct { type addOptions struct {
name string name string
noCaddy bool
sshKey string sshKey string
cluster string cluster string
} }
@@ -33,10 +44,14 @@ func NewAddCommand() *cobra.Command {
KeyPath: opts.sshKey, KeyPath: opts.sshKey,
} }
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name) return add(cmd.Context(), uncli, remoteMachine, opts)
}, },
} }
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.") cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
cmd.Flags().BoolVar(
&opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.",
)
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "", &opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", "path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
@@ -47,3 +62,87 @@ func NewAddCommand() *cobra.Command {
) )
return cmd return cmd
} }
func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, opts addOptions) error {
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name)
if err != nil {
return err
}
defer machineClient.Close()
if opts.noCaddy {
return nil
}
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
fmt.Println("Waiting for the machine to be ready...")
if err = waitClusterInitialised(ctx, machineClient); err != nil {
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
}
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
filter := func(m *pb.MachineInfo) bool {
return m.Id == minfo.Id
}
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
// use the deployed image version. Otherwise, use the latest version.
caddyImage := ""
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName)
if err != nil {
if !errors.Is(err, client.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err)
}
} else {
caddyImage = caddySvc.Containers[0].Container.Config.Image
// Find the latest created container and use its image.
var latestCreated time.Time
for _, c := range caddySvc.Containers[1:] {
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
if err != nil {
continue
}
if created.After(latestCreated) {
latestCreated = created
caddyImage = c.Container.Config.Image
}
}
}
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
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(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
}
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(30*time.Second),
), ctx)
check := func() error {
_, err := client.ListMachines(ctx)
if err == nil {
return nil
}
statusErr := status.Convert(err)
if statusErr.Code() == codes.FailedPrecondition {
return err
}
return backoff.Permanent(err)
}
return backoff.Retry(check, boff)
}
+1 -1
View File
@@ -89,7 +89,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
// Deploy the Caddy service to the initialised machine. // Deploy the Caddy service to the initialised machine.
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available // The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
// after cluster initialisation, we keep the user informed during this wait. // after cluster initialisation, we keep the user informed during this wait.
fmt.Println("Waiting for the cluster to be ready...") fmt.Println("Waiting for the machine to be ready...")
d, err := client.NewCaddyDeployment("", nil) d, err := client.NewCaddyDeployment("", nil)
if err != nil { if err != nil {
+21 -16
View File
@@ -185,39 +185,44 @@ func (cli *CLI) initRemoteMachine(
return machineClient, nil return machineClient, nil
} }
func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string) error { // TODO:
func (cli *CLI) AddMachine(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string,
) (*client.Client, error) {
c, err := cli.ConnectCluster(ctx, clusterName) c, err := cli.ConnectCluster(ctx, clusterName)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return nil, fmt.Errorf("connect to cluster: %w", err)
} }
defer func() { defer c.Close()
_ = c.Close()
}()
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
if err != nil { if err != nil {
return err return nil, err
} }
defer machineClient.Close() defer func() {
if err != nil {
machineClient.Close()
}
}()
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first. // Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{}) minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return fmt.Errorf("inspect machine: %w", err) return nil, fmt.Errorf("inspect machine: %w", err)
} }
if minfo.Id != "" { if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil { if err = cli.promptResetMachine(); err != nil {
return err return nil, err
} }
} }
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{}) tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return fmt.Errorf("get remote machine token: %w", err) return nil, fmt.Errorf("get remote machine token: %w", err)
} }
token, err := machine.ParseToken(tokenResp.Token) token, err := machine.ParseToken(tokenResp.Token)
if err != nil { if err != nil {
return fmt.Errorf("parse remote machine token: %w", err) return nil, fmt.Errorf("parse remote machine token: %w", err)
} }
// Register the machine in the cluster using its public key and endpoints from the token. // Register the machine in the cluster using its public key and endpoints from the token.
@@ -234,13 +239,13 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
} }
addResp, err := c.AddMachine(ctx, addReq) addResp, err := c.AddMachine(ctx, addReq)
if err != nil { if err != nil {
return fmt.Errorf("add machine to cluster: %w", err) return nil, fmt.Errorf("add machine to cluster: %w", err)
} }
// List other machines in the cluster to include them in the join request. // List other machines in the cluster to include them in the join request.
machines, err := c.ListMachines(ctx) machines, err := c.ListMachines(ctx)
if err != nil { if err != nil {
return fmt.Errorf("list cluster machines: %w", err) return nil, fmt.Errorf("list cluster machines: %w", err)
} }
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1) otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
for _, m := range machines { for _, m := range machines {
@@ -255,7 +260,7 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
OtherMachines: otherMachines, OtherMachines: otherMachines,
} }
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil { if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
return fmt.Errorf("join cluster: %w", err) return nil, fmt.Errorf("join cluster: %w", err)
} }
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name) fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
@@ -270,10 +275,10 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.config.Save(); err != nil {
return fmt.Errorf("save config: %w", err) return nil, fmt.Errorf("save config: %w", err)
} }
return nil return machineClient, nil
} }
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns // provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
-21
View File
@@ -232,24 +232,3 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
return &pb.ListMachinesResponse{Machines: members}, nil return &pb.ListMachinesResponse{Machines: members}, nil
} }
//func (c *Cluster) ListServices(ctx context.Context, _ *emptypb.Empty) (*pb.ListServicesResponse, error) {
// if err := c.checkInitialised(ctx); err != nil {
// return nil, err
// }
//
// return &pb.ListServicesResponse{
// Messages: []*pb.Services{
// {
// Services: []*pb.Service{
// {
// Name: "service1",
// },
// {
// Name: "service2",
// },
// },
// },
// },
// }, nil
//}