diff --git a/cmd/uncloud/main.go b/cmd/uncloud/main.go index 819af02c..9d307529 100644 --- a/cmd/uncloud/main.go +++ b/cmd/uncloud/main.go @@ -119,6 +119,7 @@ func main() { NewDocsCommand(), NewImagesCommand(), NewPsCommand(), + NewVersionCommand(), caddy.NewRootCommand(), cmdcontext.NewRootCommand(), dns.NewRootCommand(), diff --git a/cmd/uncloud/version.go b/cmd/uncloud/version.go new file mode 100644 index 00000000..fa132bdb --- /dev/null +++ b/cmd/uncloud/version.go @@ -0,0 +1,187 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/charmbracelet/huh/spinner" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" + "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/version" + "github.com/psviderski/uncloud/pkg/api" + "github.com/psviderski/uncloud/pkg/client" + "github.com/spf13/cobra" +) + +func NewVersionCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Show client and server version information.", + Long: `Show version information for both the local client and all machines in the cluster. + +The client version is always shown. If connected to a cluster, the version of the +daemon running on each machine is also displayed.`, + RunE: func(cmd *cobra.Command, args []string) error { + uncli := cmd.Context().Value("cli").(*cli.CLI) + return runVersion(cmd.Context(), uncli) + }, + } + return cmd +} + +type machineVersion struct { + name string + state string + version string +} + +func runVersion(ctx context.Context, uncli *cli.CLI) error { + fmt.Printf("Client: %s\n", versionOrUnknown(version.String())) + fmt.Println() + + // Try to connect to the cluster to get server versions. + clusterClient, err := uncli.ConnectCluster(ctx) + if err != nil { + fmt.Println("Cluster: (not connected)") + return nil + } + defer clusterClient.Close() + + var machines api.MachineMembersList + var versions map[string]string + + err = spinner.New(). + Title(" Collecting version info..."). + Type(spinner.MiniDot). + Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))). + ActionWithErr(func(ctx context.Context) error { + var err error + machines, err = clusterClient.ListMachines(ctx, nil) + if err != nil { + return fmt.Errorf("list machines: %w", err) + } + if len(machines) == 0 { + return nil + } + versions, err = inspectMachineVersions(ctx, clusterClient) + if err != nil { + return fmt.Errorf("inspect machine versions: %w", err) + } + return nil + }). + Run() + if err != nil { + return err + } + + if len(machines) == 0 { + fmt.Println("Cluster: (no machines)") + return nil + } + + // Build version info for each machine. + machineVersions := make([]machineVersion, 0, len(machines)) + for _, m := range machines { + ver := "(unreachable)" + if v, ok := versions[m.Machine.Name]; ok { + ver = v + } + machineVersions = append(machineVersions, machineVersion{ + name: m.Machine.Name, + state: capitalise(m.State.String()), + version: ver, + }) + } + + printVersions(machineVersions) + return nil +} + +func printVersions(machineVersions []machineVersion) { + t := table.New(). + Border(lipgloss.Border{}). + BorderTop(false). + BorderBottom(false). + BorderLeft(false). + BorderRight(false). + BorderHeader(false). + BorderColumn(false). + StyleFunc(func(row, col int) lipgloss.Style { + if row == table.HeaderRow { + return lipgloss.NewStyle().Bold(true).PaddingRight(3) + } + return lipgloss.NewStyle().PaddingRight(3) + }) + + t.Headers("MACHINE", "STATE", "VERSION") + + for _, mv := range machineVersions { + t.Row(mv.name, mv.state, mv.version) + } + + fmt.Println(t) +} + +// inspectMachineVersions broadcasts InspectMachine to all available machines and returns a map of machine name to version. +func inspectMachineVersions(ctx context.Context, c *client.Client) (map[string]string, error) { + // Create a context that proxies to all available (non-DOWN) machines. + proxyCtx, availableMachines, err := c.ProxyMachinesContext(ctx, nil) + if err != nil { + return nil, fmt.Errorf("proxy machines context: %w", err) + } + + // Build a map of management IP to machine name for resolving response metadata. + machineNamesByIP := make(map[string]string) + for _, m := range availableMachines { + if addr, err := m.Machine.Network.ManagementIp.ToAddr(); err == nil { + machineNamesByIP[addr.String()] = m.Machine.Name + } + } + + // Broadcast InspectMachine to all machines. + resp, err := c.MachineClient.InspectMachine(proxyCtx, nil) + if err != nil { + return nil, fmt.Errorf("inspect machines: %w", err) + } + + versions := make(map[string]string) + for _, details := range resp.Machines { + var machineName string + if details.Metadata != nil { + machineName = machineNamesByIP[details.Metadata.Machine] + if details.Metadata.Error != "" { + client.PrintWarning(fmt.Sprintf("failed to get version from machine %s: %s", + machineName, details.Metadata.Error)) + continue + } + } else if len(resp.Machines) == 1 && len(availableMachines) == 1 { + // Single machine response without metadata. + machineName = availableMachines[0].Machine.Name + } + + if machineName != "" { + versions[machineName] = versionOrUnknown(details.DaemonVersion) + } + } + + return versions, nil +} + +// versionOrUnknown returns "(unknown)" if the version is empty (e.g., old daemon without version field), +// otherwise returns the version as-is. +func versionOrUnknown(v string) string { + if v == "" { + return "(unknown)" + } + return v +} + +// capitalise returns a string where the first character is upper case, and the rest is lower case. +func capitalise(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) +} diff --git a/internal/machine/api/pb/machine.pb.go b/internal/machine/api/pb/machine.pb.go index 5107ecff..3d0914fe 100644 --- a/internal/machine/api/pb/machine.pb.go +++ b/internal/machine/api/pb/machine.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc v5.29.3 // source: internal/machine/api/pb/machine.proto package pb @@ -486,6 +486,8 @@ type MachineDetails struct { Machine *MachineInfo `protobuf:"bytes,2,opt,name=machine,proto3" json:"machine,omitempty"` // Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store. StoreDbVersion int64 `protobuf:"varint,3,opt,name=store_db_version,json=storeDbVersion,proto3" json:"store_db_version,omitempty"` + // Version of the Uncloud daemon running on the machine. + DaemonVersion string `protobuf:"bytes,4,opt,name=daemon_version,json=daemonVersion,proto3" json:"daemon_version,omitempty"` } func (x *MachineDetails) Reset() { @@ -541,6 +543,13 @@ func (x *MachineDetails) GetStoreDbVersion() int64 { return 0 } +func (x *MachineDetails) GetDaemonVersion() string { + if x != nil { + return x.DaemonVersion + } + return "" +} + type TokenResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1073,7 +1082,7 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{ 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x73, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a, + 0x6c, 0x73, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, @@ -1083,99 +1092,102 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{ 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, - 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, - 0x15, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, - 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xb2, 0x01, 0x0a, 0x1f, 0x49, 0x6e, 0x73, - 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, - 0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, - 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x83, 0x02, - 0x0a, 0x0d, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, - 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a, - 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x6c, 0x61, - 0x73, 0x74, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, - 0x6b, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, - 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, - 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, - 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x49, 0x70, 0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, - 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, - 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, - 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, - 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, + 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, + 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, + 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, + 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, + 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, + 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, + 0xb2, 0x01, 0x0a, 0x1f, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, + 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, + 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x22, 0x83, 0x02, 0x0a, 0x0d, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, + 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, + 0x61, 0x6b, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, + 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, + 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, + 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, + 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, - 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, - 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, + 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e, - 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, - 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, - 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, - 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, - 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, - 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, + 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, + 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, + 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x24, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, + 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, + 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, + 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( diff --git a/internal/machine/api/pb/machine.proto b/internal/machine/api/pb/machine.proto index 4f6451da..1a274781 100644 --- a/internal/machine/api/pb/machine.proto +++ b/internal/machine/api/pb/machine.proto @@ -78,6 +78,8 @@ message MachineDetails { MachineInfo machine = 2; // Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store. int64 store_db_version = 3; + // Version of the Uncloud daemon running on the machine. + string daemon_version = 4; } message TokenResponse { diff --git a/internal/machine/api/pb/machine_grpc.pb.go b/internal/machine/api/pb/machine_grpc.pb.go index 3f9c19bd..de4b43d6 100644 --- a/internal/machine/api/pb/machine_grpc.pb.go +++ b/internal/machine/api/pb/machine_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v5.29.3 // source: internal/machine/api/pb/machine.proto package pb diff --git a/internal/machine/machine.go b/internal/machine/machine.go index 0f726dfc..6c166fbc 100644 --- a/internal/machine/machine.go +++ b/internal/machine/machine.go @@ -30,6 +30,7 @@ import ( machinedocker "github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/store" + "github.com/psviderski/uncloud/internal/version" "github.com/psviderski/unregistry" "github.com/siderolabs/grpc-proxy/proxy" "golang.org/x/sync/errgroup" @@ -908,6 +909,7 @@ func (m *Machine) InspectMachine(ctx context.Context, _ *emptypb.Empty) (*pb.Ins }, }, StoreDbVersion: dbVersion, + DaemonVersion: version.String(), }, }, }, nil diff --git a/pkg/client/ployz/cluster.go b/pkg/client/ployz/cluster.go new file mode 100644 index 00000000..d3783fe7 --- /dev/null +++ b/pkg/client/ployz/cluster.go @@ -0,0 +1,432 @@ +package client + +import ( + "context" + "errors" + "fmt" + "net/netip" + "os" + "slices" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/docker/cli/cli/streams" + "github.com/docker/compose/v2/pkg/progress" + "github.com/psviderski/uncloud/cmd/uncloud/caddy" + "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/machine" + "github.com/psviderski/uncloud/internal/machine/api/pb" + "github.com/psviderski/uncloud/internal/sshexec" + "github.com/psviderski/uncloud/pkg/api" + "github.com/psviderski/uncloud/pkg/client" + "github.com/psviderski/uncloud/pkg/client/connector" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +type AddOptions struct { + Destination string + Name string + PublicIP string + SSHKey string + Version string + SocketPath string +} + +const ( + // PublicIPNone is the value used to indicate removal of public IP. + PublicIPNone = "none" + + installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh" + rootUser = "root" + defaultSSHPort = 22 +) + +func AddMachine(ctx context.Context, opts AddOptions) error { + socketPath := opts.SocketPath + if socketPath == "" { + socketPath = machine.DefaultUncloudSockPath + } + + // Connect to the existing cluster via Unix socket. + clusterClient, err := client.New(ctx, connector.NewUnixConnector(socketPath)) + if err != nil { + return fmt.Errorf("connect to cluster: %w", err) + } + defer clusterClient.Close() + + // Parse SSH destination. + user, host, port, err := parseSSHDestination(opts.Destination) + if err != nil { + return fmt.Errorf("parse remote machine: %w", err) + } + + // Provision and connect to the remote machine. + machineClient, err := provisionAndConnect(ctx, user, host, port, opts.SSHKey, opts.Version) + if err != nil { + return fmt.Errorf("provision machine: %w", err) + } + defer machineClient.Close() + + // Check if the machine is already initialised as a cluster member. + minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{}) + if err != nil { + return fmt.Errorf("inspect machine: %w", err) + } + if minfo.Id != "" { + // Check if the machine is already a member of this cluster. + machines, err := clusterClient.ListMachines(ctx, nil) + if err != nil { + return fmt.Errorf("list cluster machines: %w", err) + } + if slices.ContainsFunc(machines, func(m *pb.MachineMember) bool { + return m.Machine.Id == minfo.Id + }) { + return fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name) + } + + // Auto-confirm reset for programmatic usage. + if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil { + return err + } + } + + // Check machine meets all necessary system requirements before proceeding. + checkResp, err := machineClient.MachineClient.CheckPrerequisites(ctx, &emptypb.Empty{}) + if err != nil { + if status.Convert(err).Code() != codes.Unimplemented { + return fmt.Errorf("check machine prerequisites: %w", err) + } + } else if !checkResp.Satisfied { + return fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error) + } + + // Get machine token from the new machine. + tokenResp, err := machineClient.MachineClient.Token(ctx, &emptypb.Empty{}) + if err != nil { + return fmt.Errorf("get remote machine token: %w", err) + } + token, err := machine.ParseToken(tokenResp.Token) + if err != nil { + return fmt.Errorf("parse remote machine token: %w", err) + } + + // Parse public IP option. + var publicIPProto *pb.IP + switch opts.PublicIP { + case "auto": + if token.PublicIP.IsValid() { + publicIPProto = pb.NewIP(token.PublicIP) + } + case "", PublicIPNone: + publicIPProto = nil + default: + ip, err := netip.ParseAddr(opts.PublicIP) + if err != nil { + return fmt.Errorf("parse public IP: %w", err) + } + publicIPProto = pb.NewIP(ip) + } + + // Register the machine in the cluster using its public key and endpoints from the token. + endpoints := make([]*pb.IPPort, len(token.Endpoints)) + for i, addrPort := range token.Endpoints { + endpoints[i] = pb.NewIPPort(addrPort) + } + addReq := &pb.AddMachineRequest{ + Name: opts.Name, + Network: &pb.NetworkConfig{ + Endpoints: endpoints, + PublicKey: token.PublicKey, + }, + PublicIp: publicIPProto, + } + + addResp, err := clusterClient.ClusterClient.AddMachine(ctx, addReq) + if err != nil { + return fmt.Errorf("add machine to cluster: %w", err) + } + + // Get the current store DB version from the cluster to pass to the join request. + var storeDBVersion int64 + inspectResp, err := clusterClient.MachineClient.InspectMachine(ctx, &emptypb.Empty{}) + if err != nil { + if status.Convert(err).Code() != codes.Unimplemented { + return fmt.Errorf("inspect current cluster machine: %w", err) + } + } else { + storeDBVersion = inspectResp.Machines[0].StoreDbVersion + } + + // Get the most up-to-date list of other machines in the cluster to include them in the join request. + machines, err := clusterClient.ListMachines(ctx, nil) + if err != nil { + return fmt.Errorf("list cluster machines: %w", err) + } + otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1) + for _, m := range machines { + if m.Machine.Id != addResp.Machine.Id { + otherMachines = append(otherMachines, m.Machine) + } + } + + // Configure the remote machine to join the cluster. + joinReq := &pb.JoinClusterRequest{ + Machine: addResp.Machine, + OtherMachines: otherMachines, + MinStoreDbVersion: storeDBVersion, + } + if _, err = machineClient.MachineClient.JoinCluster(ctx, joinReq); err != nil { + return fmt.Errorf("join cluster: %w", err) + } + + fmt.Printf("Machine '%s' added to the cluster.\n", addResp.Machine.Name) + + // Wait for the cluster to be initialised on the machine to be able to deploy the Caddy service. + if err = machineClient.WaitClusterReady(ctx, 5*time.Minute); err != nil { + return fmt.Errorf("wait for machine to join the cluster: %w", err) + } + fmt.Println("Machine joined the cluster.") + + // Deploy Caddy service if it exists on other machines. + if err = deployCaddyIfNeeded(ctx, clusterClient); err != nil { + return err + } + + fmt.Println() + return caddy.UpdateDomainRecords(ctx, machineClient, progressOut()) +} + +func deployCaddyIfNeeded(ctx context.Context, clusterClient *client.Client) error { + caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName) + if err != nil { + if errors.Is(err, api.ErrNotFound) { + // Caddy service is not deployed. + return nil + } + return fmt.Errorf("inspect caddy service: %w", err) + } + + 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 := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{}) + if err != nil { + return fmt.Errorf("create caddy deployment: %w", err) + } + + plan, err := d.Plan(ctx) + if err != nil { + return fmt.Errorf("plan caddy deployment: %w", err) + } + + fmt.Println() + if len(plan.Operations) == 0 { + fmt.Printf("%s service is up to date.\n", client.CaddyServiceName) + return nil + } + + // Initialise a machine and container name resolver to properly format the plan output. + resolver, err := clusterClient.ServiceOperationNameResolver(ctx, caddySvc) + if err != nil { + return fmt.Errorf("create machine and container name resolver for service operations: %w", err) + } + + fmt.Println("caddy deployment plan:") + fmt.Println(plan.Format(resolver)) + fmt.Println() + + err = progress.RunWithTitle(ctx, func(ctx context.Context) error { + if _, err = d.Run(ctx); err != nil { + return fmt.Errorf("deploy caddy: %w", err) + } + return nil + }, progressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode)) + + return err +} + +func provisionAndConnect(ctx context.Context, user, host string, port int, keyPath, version string) (*client.Client, error) { + // If keyPath is actually key content, write it to a temp file. + if strings.HasPrefix(keyPath, "-----BEGIN") { + tmpFile, err := os.CreateTemp("", "ssh-key-*") + if err != nil { + return nil, fmt.Errorf("create temp key file: %w", err) + } + if _, err := tmpFile.WriteString(keyPath); err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + return nil, fmt.Errorf("write temp key file: %w", err) + } + tmpFile.Close() + if err := os.Chmod(tmpFile.Name(), 0600); err != nil { + os.Remove(tmpFile.Name()) + return nil, fmt.Errorf("chmod temp key file: %w", err) + } + keyPath = tmpFile.Name() + // Note: temp file is not cleaned up here as it may be needed by SSH connector later. + // OS will clean it up on reboot. + } + + // Connect via SSH. + sshClient, err := sshexec.Connect(user, host, port, keyPath) + // If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key. + if err != nil && keyPath == "" { + keyPath = cli.DefaultSSHKeyPath + sshClient, err = sshexec.Connect(user, host, port, keyPath) + } + if err != nil { + return nil, fmt.Errorf("SSH login to remote machine %s@%s:%d: %w", user, host, port, err) + } + + // Provision the remote machine by installing the Uncloud daemon and dependencies over SSH. + exec := sshexec.NewRemote(sshClient) + if err = provisionMachine(ctx, exec, user, version); err != nil { + return nil, fmt.Errorf("provision machine: %w", err) + } + + // Create a machine API client over a new SSH connection (to pick up group membership changes). + var machineClient *client.Client + if user == rootUser { + machineClient, err = client.New(ctx, connector.NewSSHConnectorFromClient(sshClient)) + } else { + sshConfig := &connector.SSHConnectorConfig{ + User: user, + Host: host, + Port: port, + KeyPath: keyPath, + } + machineClient, err = client.New(ctx, connector.NewSSHConnector(sshConfig)) + } + if err != nil { + return nil, fmt.Errorf("connect to remote machine: %w", err) + } + + return machineClient, nil +} + +func provisionMachine(ctx context.Context, exec sshexec.Executor, user, version string) error { + currentUser, err := exec.Run(ctx, "whoami") + if err != nil { + return fmt.Errorf("run whoami: %w", err) + } + + if currentUser != rootUser { + out, err := exec.Run(ctx, "sudo true") + if err != nil { + if strings.Contains(out, "password is required") { + return fmt.Errorf( + "user '%[1]s' requires a password for sudo, but Uncloud needs passwordless sudo or root access "+ + "to install and configure the uncloudd daemon on the remote machine.\n\n"+ + "Possible solutions:\n"+ + "1. Use root user or a user with passwordless sudo instead.\n"+ + "2. Configure passwordless sudo for the user '%[1]s' by running on the remote machine:\n"+ + " echo '%[1]s ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/%[1]s", + currentUser) + } + return fmt.Errorf("sudo command failed for user '%s': %w. "+ + "Please ensure the user has sudo privileges or use root user instead", currentUser, err) + } + } + + cmd := installCmd(user, version) + + fmt.Println("Downloading Uncloud install script:", installScriptURL) + + cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd) + if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil { + return fmt.Errorf("download and run install script: %w", err) + } + return nil +} + +func installCmd(user string, version string) string { + sudoPrefix := "" + var env []string + + if user != rootUser { + sudoPrefix = "sudo" + env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user)) + } + if version != "" { + env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version)) + } + + envCmd := strings.Join(env, " ") + return fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd) +} + +func resetAndWaitMachine(ctx context.Context, machineClient pb.MachineClient) error { + if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil { + return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+ + "on the remote machine to fully uninstall Uncloud from it", err) + } + + fmt.Println("Resetting the remote machine...") + if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil { + return fmt.Errorf("wait for machine to be ready after reset: %w", err) + } + + return nil +} + +func waitMachineReady(ctx context.Context, machineClient pb.MachineClient, timeout time.Duration) error { + boff := backoff.WithContext(backoff.NewExponentialBackOff( + backoff.WithMaxInterval(1*time.Second), + backoff.WithMaxElapsedTime(timeout), + ), ctx) + + inspect := func() error { + _, err := machineClient.Inspect(ctx, &emptypb.Empty{}) + if err != nil { + return fmt.Errorf("inspect machine: %w", err) + } + return nil + } + return backoff.Retry(inspect, boff) +} + +func parseSSHDestination(dest string) (user, host string, port int, err error) { + port = defaultSSHPort + user = "root" + + // Handle user@host format. + if idx := strings.LastIndex(dest, "@"); idx != -1 { + user = dest[:idx] + dest = dest[idx+1:] + } + + // Handle host:port format. + if idx := strings.LastIndex(dest, ":"); idx != -1 { + host = dest[:idx] + _, err = fmt.Sscanf(dest[idx+1:], "%d", &port) + if err != nil { + return "", "", 0, fmt.Errorf("invalid port in destination: %s", dest) + } + } else { + host = dest + } + + if host == "" { + return "", "", 0, fmt.Errorf("empty host in destination") + } + + return user, host, port, nil +} + +func progressOut() *streams.Out { + return streams.NewOut(os.Stdout) +}