feat: impl of functionality for renaming and updating machines (#91)

This commit is contained in:
Evgenii Orlov
2025-07-22 18:45:19 +10:00
committed by GitHub
parent da3634b690
commit 2c3bea64e5
15 changed files with 1121 additions and 62 deletions
+3 -1
View File
@@ -37,7 +37,9 @@ jobs:
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
- name: Run tests
run: make test
run: |
make ucind-image
make test
timeout-minutes: 10
check-protobuf:
+10 -1
View File
@@ -46,6 +46,11 @@ proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
.PHONY: proto-mise
proto-mise:
mise exec -- protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
.PHONY: corrosion-image
corrosion-image:
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
@@ -67,9 +72,13 @@ test:
ifeq ($(TEST_NAME),)
go test -count=1 -v ./...
else
go test -count=1 -v -run ^$(TEST_NAME)$$ ./...
go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
endif
.PHONY: test-e2e
test-e2e:
go test -race -count=1 -v ./test/e2e
.PHONY: test-clean
test-clean:
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
+2 -2
View File
@@ -59,7 +59,7 @@ func NewAddCommand() *cobra.Command {
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
@@ -82,7 +82,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
case "", PublicIPNone:
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
+6
View File
@@ -0,0 +1,6 @@
package machine
const (
// PublicIPNone is the value used to indicate removal of public IP
PublicIPNone = "none"
)
+2 -2
View File
@@ -77,7 +77,7 @@ func NewInitCommand() *cobra.Command {
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
@@ -105,7 +105,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
case "", PublicIPNone:
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
+47
View File
@@ -0,0 +1,47 @@
package machine
import (
"context"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewRenameCommand() *cobra.Command {
var contextName string
cmd := &cobra.Command{
Use: "rename OLD_NAME NEW_NAME",
Short: "Rename a machine in the cluster.",
Long: `Rename a machine in the cluster.
This command changes the name of an existing machine while preserving all other
configuration including network settings, public IP, and cluster membership.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return rename(cmd.Context(), uncli, contextName, args[0], args[1])
},
}
cmd.Flags().StringVarP(
&contextName, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func rename(ctx context.Context, uncli *cli.CLI, contextName, oldName, newName string) error {
client, err := uncli.ConnectCluster(ctx, contextName)
if err != nil {
return err
}
defer client.Close()
machine, err := client.RenameMachine(ctx, oldName, newName)
if err != nil {
return fmt.Errorf("rename machine: %w", err)
}
fmt.Printf("Machine %q renamed to %q (ID: %s)\n", oldName, machine.Name, machine.Id)
return nil
}
+2
View File
@@ -14,7 +14,9 @@ func NewRootCommand() *cobra.Command {
NewAddCommand(),
NewInitCommand(),
NewListCommand(),
NewRenameCommand(),
NewRmCommand(),
NewUpdateCommand(),
NewTokenCommand(),
)
return cmd
+128
View File
@@ -0,0 +1,128 @@
package machine
import (
"context"
"fmt"
"net/netip"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/spf13/cobra"
)
type updateOptions struct {
name string
publicIP string
context string
}
func NewUpdateCommand() *cobra.Command {
opts := updateOptions{}
cmd := &cobra.Command{
Use: "update",
Short: "Update machine configuration in the cluster.",
Long: `Update machine configuration in the cluster.
This command allows setting various machine properties including:
- Machine name (--name)
- Public IP address (--public-ip)
At least one flag must be specified to perform an update operation.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return update(cmd.Context(), uncli, cmd, opts, args[0])
},
}
cmd.Flags().StringVar(
&opts.name, "name", "",
"New name for the machine",
)
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "",
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
)
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error {
// Check if at least one flag was explicitly set
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") {
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
}
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return err
}
defer client.Close()
// First, resolve the machine to get its ID
machine, err := client.InspectMachine(ctx, machineNameOrID)
if err != nil {
return fmt.Errorf("find machine: %w", err)
}
// Build the update request
req := &pb.UpdateMachineRequest{
MachineId: machine.Machine.Id,
}
if opts.name != "" {
req.Name = &opts.name
}
// Check if --public-ip flag was explicitly provided
if cmd.Flags().Changed("public-ip") {
if opts.publicIP == "" || opts.publicIP == PublicIPNone {
req.PublicIp = &pb.IP{} // Empty IP to signal removal
} else {
// Parse and validate the public IP
ip, err := netip.ParseAddr(opts.publicIP)
if err != nil {
return fmt.Errorf("invalid public IP address %q: %w", opts.publicIP, err)
}
req.PublicIp = pb.NewIP(ip)
}
}
// Perform the update operation
updatedMachine, err := client.UpdateMachine(ctx, req)
if err != nil {
return fmt.Errorf("update machine: %w", err)
}
// Report what was changed
changes := make([]string, 0)
if opts.name != "" {
changes = append(changes, fmt.Sprintf("name: %q -> %q", machine.Machine.Name, updatedMachine.Name))
}
if cmd.Flags().Changed("public-ip") {
oldIP := PublicIPNone
if machine.Machine.PublicIp != nil {
if addr, err := machine.Machine.PublicIp.ToAddr(); err == nil {
oldIP = addr.String()
}
}
newIP := PublicIPNone
if updatedMachine.PublicIp != nil {
if addr, err := updatedMachine.PublicIp.ToAddr(); err == nil {
newIP = addr.String()
}
}
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
}
fmt.Printf("Machine %q (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
for _, change := range changes {
fmt.Printf(" %s\n", change)
}
return nil
}
+229 -55
View File
@@ -590,6 +590,126 @@ func (x *DNSRecord) GetValues() []string {
return nil
}
type UpdateMachineRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Machine to update
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
// Updated machine information
Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"`
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof" json:"public_ip,omitempty"`
Endpoints []*IPPort `protobuf:"bytes,4,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
}
func (x *UpdateMachineRequest) Reset() {
*x = UpdateMachineRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpdateMachineRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateMachineRequest) ProtoMessage() {}
func (x *UpdateMachineRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateMachineRequest.ProtoReflect.Descriptor instead.
func (*UpdateMachineRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
}
func (x *UpdateMachineRequest) GetMachineId() string {
if x != nil {
return x.MachineId
}
return ""
}
func (x *UpdateMachineRequest) GetName() string {
if x != nil && x.Name != nil {
return *x.Name
}
return ""
}
func (x *UpdateMachineRequest) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
func (x *UpdateMachineRequest) GetEndpoints() []*IPPort {
if x != nil {
return x.Endpoints
}
return nil
}
type UpdateMachineResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
}
func (x *UpdateMachineResponse) Reset() {
*x = UpdateMachineResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpdateMachineResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateMachineResponse) ProtoMessage() {}
func (x *UpdateMachineResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateMachineResponse.ProtoReflect.Descriptor instead.
func (*UpdateMachineResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{10}
}
func (x *UpdateMachineResponse) GetMachine() *MachineInfo {
if x != nil {
return x.Machine
}
return nil
}
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
@@ -654,35 +774,56 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f,
0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02,
0x32, 0x86, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a,
0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37,
0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f,
0x6d, 0x61, 0x69, 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, 0x0b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c,
0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 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, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
0x22, 0xbb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01,
0x01, 0x12, 0x29, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x01, 0x52,
0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x09,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x22, 0x43,
0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x32, 0xce, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41,
0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 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, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x46, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73,
0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61,
0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 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, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44,
0x6f, 0x6d, 0x61, 0x69, 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, 0x0b, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x73, 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,
0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 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 (
@@ -698,7 +839,7 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
}
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
@@ -711,38 +852,46 @@ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(*CreateDomainRecordsRequest)(nil), // 8: api.CreateDomainRecordsRequest
(*CreateDomainRecordsResponse)(nil), // 9: api.CreateDomainRecordsResponse
(*DNSRecord)(nil), // 10: api.DNSRecord
(*NetworkConfig)(nil), // 11: api.NetworkConfig
(*IP)(nil), // 12: api.IP
(*MachineInfo)(nil), // 13: api.MachineInfo
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
(*UpdateMachineRequest)(nil), // 11: api.UpdateMachineRequest
(*UpdateMachineResponse)(nil), // 12: api.UpdateMachineResponse
(*NetworkConfig)(nil), // 13: api.NetworkConfig
(*IP)(nil), // 14: api.IP
(*MachineInfo)(nil), // 15: api.MachineInfo
(*IPPort)(nil), // 16: api.IPPort
(*emptypb.Empty)(nil), // 17: google.protobuf.Empty
}
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
13, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
14, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
15, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
15, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
14, // 13: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
8, // 14: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
3, // 15: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
5, // 16: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
6, // 17: api.Cluster.ReserveDomain:output_type -> api.Domain
6, // 18: api.Cluster.GetDomain:output_type -> api.Domain
6, // 19: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // 20: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
15, // [15:21] is the sub-list for method output_type
9, // [9:15] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
14, // 9: api.UpdateMachineRequest.public_ip:type_name -> api.IP
16, // 10: api.UpdateMachineRequest.endpoints:type_name -> api.IPPort
15, // 11: api.UpdateMachineResponse.machine:type_name -> api.MachineInfo
2, // 12: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
17, // 13: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
11, // 14: api.Cluster.UpdateMachine:input_type -> api.UpdateMachineRequest
7, // 15: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
17, // 16: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
17, // 17: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
8, // 18: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
3, // 19: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
5, // 20: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
12, // 21: api.Cluster.UpdateMachine:output_type -> api.UpdateMachineResponse
6, // 22: api.Cluster.ReserveDomain:output_type -> api.Domain
6, // 23: api.Cluster.GetDomain:output_type -> api.Domain
6, // 24: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // 25: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
19, // [19:26] is the sub-list for method output_type
12, // [12:19] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_internal_machine_api_pb_cluster_proto_init() }
@@ -861,14 +1010,39 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*UpdateMachineRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*UpdateMachineResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[9].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 2,
NumMessages: 9,
NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
+15
View File
@@ -11,6 +11,7 @@ import "internal/machine/api/pb/machine.proto";
service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
rpc GetDomain(google.protobuf.Empty) returns (Domain);
@@ -76,3 +77,17 @@ message DNSRecord {
RecordType type = 2;
repeated string values = 3;
}
message UpdateMachineRequest {
// Machine to update
string machine_id = 1;
// Updated machine information
optional string name = 2;
optional IP public_ip = 3;
repeated IPPort endpoints = 4;
}
message UpdateMachineResponse {
MachineInfo machine = 1;
}
@@ -22,6 +22,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_UpdateMachine_FullMethodName = "/api.Cluster/UpdateMachine"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
@@ -34,6 +35,7 @@ const (
type ClusterClient interface {
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
@@ -68,6 +70,16 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
return out, nil
}
func (c *clusterClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMachineResponse)
err := c.cc.Invoke(ctx, Cluster_UpdateMachine_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
@@ -114,6 +126,7 @@ func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomai
type ClusterServer interface {
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
@@ -134,6 +147,9 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
}
func (UnimplementedClusterServer) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented")
}
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
}
@@ -203,6 +219,24 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _Cluster_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).UpdateMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_UpdateMachine_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).UpdateMachine(ctx, req.(*UpdateMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReserveDomainRequest)
if err := dec(in); err != nil {
@@ -290,6 +324,10 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines",
Handler: _Cluster_ListMachines_Handler,
},
{
MethodName: "UpdateMachine",
Handler: _Cluster_UpdateMachine_Handler,
},
{
MethodName: "ReserveDomain",
Handler: _Cluster_ReserveDomain_Handler,
+83
View File
@@ -198,6 +198,89 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
return resp, nil
}
// UpdateMachine updates machine configuration in the cluster.
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
if req.MachineId == "" {
return nil, status.Error(codes.InvalidArgument, "machine_id not set")
}
// Get the current machine info
currentMachine, err := c.store.GetMachine(ctx, req.MachineId)
if err != nil {
if errors.Is(err, store.ErrMachineNotFound) {
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.MachineId)
}
return nil, status.Errorf(codes.Internal, "failed to get machine: %v", err)
}
// Create a copy of the current machine for updating
updatedMachine := &pb.MachineInfo{
Id: currentMachine.Id,
Name: currentMachine.Name,
Network: currentMachine.Network,
PublicIp: currentMachine.PublicIp,
}
// Apply updates from the request
if req.Name != nil {
// Check for empty name
if *req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "machine name cannot be empty")
}
// Check for duplicate names (excluding the current machine)
if *req.Name != currentMachine.Name {
machines, err := c.store.ListMachines(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "list machines: %v", err)
}
for _, m := range machines {
if m.Id != req.MachineId && m.Name == *req.Name {
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", *req.Name)
}
}
}
updatedMachine.Name = *req.Name
}
if req.PublicIp != nil {
// Check if this is an empty IP (used to signal removal)
if len(req.PublicIp.Ip) == 0 {
// User wants to remove public IP
updatedMachine.PublicIp = nil
} else {
// Validate and set the new IP
ip, err := req.PublicIp.ToAddr()
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid public IP: %v", err)
}
if !ip.IsValid() {
return nil, status.Error(codes.InvalidArgument, "invalid public IP")
}
updatedMachine.PublicIp = req.PublicIp
}
}
if req.Endpoints != nil {
updatedMachine.Network.Endpoints = req.Endpoints
}
// Update the machine in the store
if err = c.store.UpdateMachine(ctx, updatedMachine); err != nil {
if errors.Is(err, store.ErrMachineNotFound) {
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.MachineId)
}
return nil, status.Errorf(codes.Internal, "update machine: %v", err)
}
slog.Info("Machine configuration updated in the cluster.",
"id", updatedMachine.Id, "name", updatedMachine.Name)
resp := &pb.UpdateMachineResponse{Machine: updatedMachine}
return resp, nil
}
// ListMachines lists all machines in the cluster including their membership states.
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
if err := c.checkInitialised(ctx); err != nil {
+75 -1
View File
@@ -16,7 +16,8 @@ var (
//go:embed schema.sql
Schema string
ErrKeyNotFound = errors.New("key not found")
ErrKeyNotFound = errors.New("key not found")
ErrMachineNotFound = errors.New("machine not found")
)
// Store is a cluster store backed by a distributed Corrosion database.
@@ -67,6 +68,79 @@ func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
return nil
}
func (s *Store) UpdateMachine(ctx context.Context, m *pb.MachineInfo) error {
if m == nil {
return fmt.Errorf("machine info cannot be nil")
}
if m.Id == "" {
return fmt.Errorf("machine ID cannot be empty")
}
mJSON, err := protojson.Marshal(m)
if err != nil {
return fmt.Errorf("marshal machine info: %w", err)
}
result, err := s.corro.ExecContext(ctx, "UPDATE machines SET info = ? WHERE id = ?", string(mJSON), m.Id)
if err != nil {
return fmt.Errorf("update machine: %w", err)
}
// Check if machine exists
if result.RowsAffected == 0 {
return fmt.Errorf("%w: %s", ErrMachineNotFound, m.Id)
}
return nil
}
func (s *Store) GetMachine(ctx context.Context, machineID string) (*pb.MachineInfo, error) {
if machineID == "" {
return nil, fmt.Errorf("machine ID cannot be empty")
}
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines WHERE id = ?", machineID)
if err != nil {
return nil, fmt.Errorf("query machine: %w", err)
}
defer rows.Close()
if !rows.Next() {
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("query error: %w", err)
}
return nil, fmt.Errorf("%w: %s", ErrMachineNotFound, machineID)
}
var mJSON string
if err = rows.Scan(&mJSON); err != nil {
return nil, fmt.Errorf("scan machine info: %w", err)
}
if mJSON == "" {
return nil, fmt.Errorf("machine info is empty for id %s", machineID)
}
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
var m pb.MachineInfo
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
return nil, fmt.Errorf("unmarshal machine info for id %s: %w", machineID, err)
}
// Validate the unmarshaled data. just in case
if m.Id != machineID {
return nil, fmt.Errorf("machine ID mismatch: expected %s, got %s", machineID, m.Id)
}
if m.Network != nil {
if err = m.Network.Validate(); err != nil {
return nil, fmt.Errorf("invalid network configuration for machine %s: %w", m.Id, err)
}
}
return &m, nil
}
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name")
if err != nil {
+31
View File
@@ -6,6 +6,8 @@ import (
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -46,6 +48,35 @@ func (cli *Client) ListMachines(ctx context.Context, filter *api.MachineFilter)
return machines, nil
}
// UpdateMachine updates machine configuration in the cluster.
func (cli *Client) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.MachineInfo, error) {
resp, err := cli.ClusterClient.UpdateMachine(ctx, req)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
return nil, api.ErrNotFound
}
return nil, err
}
return resp.Machine, nil
}
// RenameMachine renames an existing machine in the cluster.
func (cli *Client) RenameMachine(ctx context.Context, nameOrID, newName string) (*pb.MachineInfo, error) {
// First, resolve the machine to get its ID
machine, err := cli.InspectMachine(ctx, nameOrID)
if err != nil {
return nil, err
}
// Update the machine with the new name
req := &pb.UpdateMachineRequest{
MachineId: machine.Machine.Id,
Name: &newName,
}
return cli.UpdateMachine(ctx, req)
}
func MachineMatchesFilter(machine *pb.MachineMember, filter *api.MachineFilter) bool {
if filter == nil {
return true
+450
View File
@@ -0,0 +1,450 @@
package e2e
import (
"context"
"errors"
"testing"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMachineRename(t *testing.T) {
name := "ucind-test.machine-rename"
ctx := context.Background()
c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true)
cli, err := c.Machines[0].Connect(ctx)
require.NoError(t, err)
defer cli.Close()
t.Run("rename machine by name", func(t *testing.T) {
// Get initial machine state
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
// Select the second machine to rename
originalMachine := machines[1]
originalName := originalMachine.Machine.Name
// nolint:goconst
newName := "renamed-machine-1"
// Rename the machine
updatedMachine, err := cli.RenameMachine(ctx, originalName, newName)
require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, originalMachine.Machine.Id, updatedMachine.Id)
// Verify the machine list reflects the change
machines, err = cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
// Find the renamed machine
var found bool
for _, m := range machines {
if m.Machine.Id == originalMachine.Machine.Id {
assert.Equal(t, newName, m.Machine.Name)
found = true
} else {
// Ensure other machines are unaffected
assert.NotEqual(t, newName, m.Machine.Name)
}
}
assert.True(t, found, "Renamed machine should be in the list")
// Verify we can inspect the machine by its new name
inspectedMachine, err := cli.InspectMachine(ctx, newName)
require.NoError(t, err)
assert.Equal(t, newName, inspectedMachine.Machine.Name)
assert.Equal(t, originalMachine.Machine.Id, inspectedMachine.Machine.Id)
// Verify the old name no longer works
_, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound)
})
t.Run("rename machine by ID", func(t *testing.T) {
// Get the third machine
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
// Find a machine that hasn't been renamed yet
var targetMachine *pb.MachineMember
for _, m := range machines {
if m.Machine.Name != "renamed-machine-1" {
targetMachine = m
break
}
}
require.NotNil(t, targetMachine)
originalName := targetMachine.Machine.Name
machineID := targetMachine.Machine.Id
newName := "renamed-machine-2"
// Rename using ID instead of name
updatedMachine, err := cli.RenameMachine(ctx, machineID, newName)
require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, machineID, updatedMachine.Id)
// Verify the rename was successful
inspectedMachine, err := cli.InspectMachine(ctx, newName)
require.NoError(t, err)
assert.Equal(t, newName, inspectedMachine.Machine.Name)
assert.Equal(t, machineID, inspectedMachine.Machine.Id)
// Verify the old name no longer works
_, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound)
})
t.Run("rename non-existent machine", func(t *testing.T) {
// Try to rename a machine that doesn't exist
_, err := cli.RenameMachine(ctx, "non-existent-machine", "new-name")
assert.ErrorIs(t, err, api.ErrNotFound)
// Try with a non-existent ID
_, err = cli.RenameMachine(ctx, "non-existent-id-12345", "new-name")
assert.ErrorIs(t, err, api.ErrNotFound)
})
t.Run("rename to existing name", func(t *testing.T) {
// Get current machines
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
// Try to rename machine 0 to the name of machine 1
machine0Name := machines[0].Machine.Name
machine1Name := machines[1].Machine.Name
// This should fail because the name is already taken
_, err = cli.RenameMachine(ctx, machine0Name, machine1Name)
assert.Error(t, err)
})
t.Run("rename with empty name", func(t *testing.T) {
// Get a machine to rename
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
machineName := machines[0].Machine.Name
// Try to rename with empty string
_, err = cli.RenameMachine(ctx, machineName, "")
assert.Error(t, err)
})
t.Run("service continuity after rename", func(t *testing.T) {
// Deploy a service on a specific machine
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
// Find a machine that hasn't been renamed to test with
var targetMachine *pb.MachineMember
for _, m := range machines {
if m.Machine.Name != "renamed-machine-1" && m.Machine.Name != "renamed-machine-2" {
targetMachine = m
break
}
}
require.NotNil(t, targetMachine)
originalMachineName := targetMachine.Machine.Name
serviceName := "test-service-rename-continuity"
// Create a service on the specific machine
spec := api.ServiceSpec{
Name: serviceName,
Mode: api.ServiceModeGlobal,
Container: api.ContainerSpec{
Image: "portainer/pause:latest",
},
Placement: api.Placement{
Machines: []string{originalMachineName},
},
}
_, err = cli.RunService(ctx, spec)
require.NoError(t, err)
t.Cleanup(func() {
err := cli.RemoveService(ctx, serviceName)
if err != nil && !errors.Is(err, api.ErrNotFound) {
assert.NoError(t, err)
}
})
// Verify service is running on the machine
svc, err := cli.InspectService(ctx, serviceName)
require.NoError(t, err)
assert.Len(t, svc.Containers, 1)
assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID)
// Rename the machine
newMachineName := "renamed-for-service-test"
_, err = cli.RenameMachine(ctx, originalMachineName, newMachineName)
require.NoError(t, err)
// Verify service is still running on the renamed machine
svc, err = cli.InspectService(ctx, serviceName)
require.NoError(t, err)
assert.Len(t, svc.Containers, 1)
assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID)
// The service spec's placement still references the old name,
// but the service should continue to run on the same machine id
})
}
func TestUpdateMachine(t *testing.T) {
name := "ucind-test.machine-update"
ctx := context.Background()
c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true)
cli, err := c.Machines[0].Connect(ctx)
require.NoError(t, err)
defer cli.Close()
t.Run("update machine name", func(t *testing.T) {
// Get initial machine state
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
// Select a machine to update
targetMachine := machines[1]
originalName := targetMachine.Machine.Name
//TODO: From my point of view, the most correct option is to use exclusions.rules list for `_test\.go$` files
// nolint:goconst
newName := "updated-machine-name"
// Update the machine name using UpdateMachine directly
req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id,
Name: &newName,
}
updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, targetMachine.Machine.Id, updatedMachine.Id)
// Verify the change persisted
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err)
assert.Equal(t, newName, inspected.Machine.Name)
// Verify old name no longer works
_, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound)
})
t.Run("update machine public IP", func(t *testing.T) {
// Get a machine to update
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
// Find a machine that hasn't been renamed
var targetMachine *pb.MachineMember
for _, m := range machines {
if m.Machine.Name != "updated-machine-name" {
targetMachine = m
break
}
}
require.NotNil(t, targetMachine)
// Create a new public IP (must be a valid public IP address)
newPublicIP := &pb.IP{
Ip: []byte{8, 8, 8, 8},
}
// Update the public IP
req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id,
PublicIp: newPublicIP,
}
updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err)
assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip)
// Verify the change persisted
inspected, err := cli.InspectMachine(ctx, targetMachine.Machine.Id)
require.NoError(t, err)
assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip)
})
t.Run("remove machine public IP", func(t *testing.T) {
// Get machines
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.True(t, len(machines) > 0, "Need at least one machine")
// First, set a public IP on a machine
targetMachine := machines[0]
setIPReq := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id,
PublicIp: &pb.IP{Ip: []byte{192, 0, 2, 1}}, // TEST-NET-1 address
}
updatedMachine, err := cli.UpdateMachine(ctx, setIPReq)
require.NoError(t, err)
require.NotNil(t, updatedMachine.PublicIp)
// Now test removing the public IP
// Remove the public IP by setting it to empty
emptyIP := &pb.IP{}
req := &pb.UpdateMachineRequest{
MachineId: updatedMachine.Id,
PublicIp: emptyIP,
}
removedIPMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err)
assert.Nil(t, removedIPMachine.PublicIp)
// Verify the change persisted
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err)
assert.Nil(t, inspected.Machine.PublicIp)
})
t.Run("update machine endpoints", func(t *testing.T) {
// Get a machine to update
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
var targetMachine *pb.MachineMember
for _, m := range machines {
targetMachine = m
break
}
require.NotNil(t, targetMachine)
newEndpoints := []*pb.IPPort{
{
Ip: &pb.IP{Ip: []byte{10, 0, 0, 10}},
Port: 8080,
},
{
Ip: &pb.IP{Ip: []byte{10, 0, 0, 10}},
Port: 8443,
},
}
req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id,
Endpoints: newEndpoints,
}
updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err)
assert.Equal(t, len(newEndpoints), len(updatedMachine.Network.Endpoints))
// Verify endpoints were updated
for i, endpoint := range updatedMachine.Network.Endpoints {
assert.Equal(t, newEndpoints[i].Ip.Ip, endpoint.Ip.Ip)
assert.Equal(t, newEndpoints[i].Port, endpoint.Port)
}
// Verify other network fields remain unchanged
assert.Equal(t, targetMachine.Machine.Network.Subnet.Ip.Ip, updatedMachine.Network.Subnet.Ip.Ip)
assert.Equal(t, targetMachine.Machine.Network.Subnet.Bits, updatedMachine.Network.Subnet.Bits)
assert.Equal(t, targetMachine.Machine.Network.ManagementIp.Ip, updatedMachine.Network.ManagementIp.Ip)
assert.Equal(t, targetMachine.Machine.Network.PublicKey, updatedMachine.Network.PublicKey)
})
t.Run("update multiple fields simultaneously", func(t *testing.T) {
// Get a machine to update
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
var targetMachine *pb.MachineMember
for _, m := range machines {
if m.Machine.Name != "updated-machine-name" {
targetMachine = m
break
}
}
require.NotNil(t, targetMachine)
// Update both name and public IP
newName := "multi-update-machine"
newPublicIP := &pb.IP{
Ip: []byte{1, 1, 1, 1},
}
req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id,
Name: &newName,
PublicIp: newPublicIP,
}
updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip)
// Verify both changes persisted
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err)
assert.Equal(t, newName, inspected.Machine.Name)
assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip)
})
t.Run("update non-existent machine", func(t *testing.T) {
// Try to update properties on a machine that doesn't exist
nonExistentName := "should-be-updated"
req := &pb.UpdateMachineRequest{
MachineId: "non-existent-machine-id",
Name: &nonExistentName,
}
_, err := cli.UpdateMachine(ctx, req)
assert.Error(t, err)
})
t.Run("update to duplicate name", func(t *testing.T) {
// Get two machines
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
require.Len(t, machines, 3)
machine1 := machines[0]
machine2 := machines[1]
// Try to update machine2 with machine1's name
req := &pb.UpdateMachineRequest{
MachineId: machine2.Machine.Id,
Name: &machine1.Machine.Name,
}
_, err = cli.UpdateMachine(ctx, req)
assert.Error(t, err)
})
t.Run("update with empty request", func(t *testing.T) {
// Get a machine
machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err)
targetMachine := machines[0]
// Update with no fields set (should be a no-op)
req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id,
}
updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err)
// Machine should remain unchanged
assert.Equal(t, targetMachine.Machine.Name, updatedMachine.Name)
if targetMachine.Machine.PublicIp != nil && updatedMachine.PublicIp != nil {
assert.Equal(t, targetMachine.Machine.PublicIp.Ip, updatedMachine.PublicIp.Ip)
}
assert.Equal(t, len(targetMachine.Machine.Network.Endpoints), len(updatedMachine.Network.Endpoints))
})
}