mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
chore(volumes): add client methods for volume management, e2e test
This commit is contained in:
+1
-1
@@ -39,7 +39,7 @@ type ImageClient interface {
|
||||
|
||||
type MachineClient interface {
|
||||
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
|
||||
ListMachines(ctx context.Context) ([]*pb.MachineMember, error)
|
||||
ListMachines(ctx context.Context) (MachineMembersList, error)
|
||||
}
|
||||
|
||||
type ServiceClient interface {
|
||||
|
||||
@@ -110,11 +110,6 @@ type ServiceContainer struct {
|
||||
ServiceSpec ServiceSpec
|
||||
}
|
||||
|
||||
type MachineContainer struct {
|
||||
MachineID string
|
||||
Container Container
|
||||
}
|
||||
|
||||
// ServiceID returns the ID of the service this container belongs to.
|
||||
func (c *ServiceContainer) ServiceID() string {
|
||||
return c.Config.Labels[LabelServiceID]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import "github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
|
||||
type MachineMembersList []*pb.MachineMember
|
||||
|
||||
func (m MachineMembersList) FindByManagementIP(ip string) *pb.MachineMember {
|
||||
for _, machine := range m {
|
||||
addr, err := machine.Machine.Network.ManagementIp.ToAddr()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.String() == ip {
|
||||
return machine
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,6 +44,8 @@ type BindOptions struct {
|
||||
// VolumeOptions represents options for a managed volume.
|
||||
type VolumeOptions struct {
|
||||
// Driver specifies the volume driver and its options for volume creation.
|
||||
// TODO: It seems we don't really need Driver and Labels if we only support externally managed volumes.
|
||||
// However we may need them in the future if we add support for isolated container-scoped volumes.
|
||||
Driver *mount.Driver `json:",omitempty"`
|
||||
// Labels are key-value metadata to apply to the volume if creating a new volume.
|
||||
Labels map[string]string `json:",omitempty"`
|
||||
@@ -182,3 +185,13 @@ func sortVolumeMounts(mounts []VolumeMount) {
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// MachineVolume represents a volume on a specific machine.
|
||||
type MachineVolume struct {
|
||||
// MachineID is the ID of the machine where the volume exists.
|
||||
MachineID string
|
||||
// MachineName is the name of the machine where the volume exists.
|
||||
MachineName string
|
||||
// Volume is the Docker volume model.
|
||||
Volume volume.Volume
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
||||
func (cli *Client) InspectMachine(ctx context.Context, nameOrID string) (*pb.MachineMember, error) {
|
||||
machines, err := cli.ListMachines(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range machines {
|
||||
if m.Machine.Id == id || m.Machine.Name == id {
|
||||
if m.Machine.Id == nameOrID || m.Machine.Name == nameOrID {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
@@ -22,7 +23,8 @@ func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMe
|
||||
return nil, api.ErrNotFound
|
||||
}
|
||||
|
||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
||||
// ListMachines returns a list of all machines registered in the cluster.
|
||||
func (cli *Client) ListMachines(ctx context.Context) (api.MachineMembersList, error) {
|
||||
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func PrintWarning(msg string) {
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color("11")) // Bright yellow.
|
||||
styledMsg := style.Render(fmt.Sprintf("WARNING: %s", msg))
|
||||
fmt.Fprintln(os.Stderr, styledMsg)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
// CreateVolume creates a new volume on the specified machine.
|
||||
func (cli *Client) CreateVolume(
|
||||
ctx context.Context, machineNameOrID string, opts volume.CreateOptions,
|
||||
) (api.MachineVolume, error) {
|
||||
var resp api.MachineVolume
|
||||
|
||||
if opts.Name == "" {
|
||||
return resp, fmt.Errorf("volume name is required (anonymous volumes are not supported)")
|
||||
}
|
||||
|
||||
machine, err := cli.InspectMachine(ctx, machineNameOrID)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("inspect machine '%s': %w", machineNameOrID, err)
|
||||
}
|
||||
// Proxy Docker gRPC requests to the selected machine.
|
||||
ctx = proxyToMachine(ctx, machine.Machine)
|
||||
|
||||
pw := progress.ContextWriter(ctx)
|
||||
eventID := fmt.Sprintf("Volume %s on %s", opts.Name, machine.Machine.Name)
|
||||
pw.Event(progress.CreatingEvent(eventID))
|
||||
|
||||
vol, err := cli.Docker.CreateVolume(ctx, opts)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("create volume on machine '%s': %w", machine.Machine.Name, err)
|
||||
}
|
||||
|
||||
resp = api.MachineVolume{
|
||||
MachineID: machine.Machine.Id,
|
||||
MachineName: machine.Machine.Name,
|
||||
Volume: vol,
|
||||
}
|
||||
pw.Event(progress.CreatedEvent(eventID))
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListVolumes returns a list of all volumes on the cluster machines.
|
||||
func (cli *Client) ListVolumes(ctx context.Context) ([]api.MachineVolume, error) {
|
||||
machines, err := cli.ListMachines(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
|
||||
// Broadcast the volume list request to all machines.
|
||||
listCtx, err := api.ProxyMachinesContext(ctx, cli, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
||||
}
|
||||
|
||||
machineVolumes, err := cli.Docker.ListVolumes(listCtx, volume.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list volumes: %w", err)
|
||||
}
|
||||
|
||||
var volumes []api.MachineVolume
|
||||
// Process responses from all machines.
|
||||
for _, mv := range machineVolumes {
|
||||
if mv.Metadata != nil && mv.Metadata.Error != "" {
|
||||
// TODO: return failed machines in the response.
|
||||
PrintWarning(fmt.Sprintf("failed to list volumes on machine '%s': %s",
|
||||
mv.Metadata.Machine, mv.Metadata.Error))
|
||||
continue
|
||||
}
|
||||
|
||||
var m *pb.MachineMember
|
||||
if mv.Metadata == nil {
|
||||
// ListVolumes was proxied to only one machine.
|
||||
m = machines[0]
|
||||
} else {
|
||||
m = machines.FindByManagementIP(mv.Metadata.Machine)
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("machine not found by management IP: %s", mv.Metadata.Machine)
|
||||
}
|
||||
}
|
||||
|
||||
for _, vol := range mv.Response.Volumes {
|
||||
volumes = append(volumes, api.MachineVolume{
|
||||
MachineID: m.Machine.Id,
|
||||
MachineName: m.Machine.Name,
|
||||
Volume: *vol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// RemoveVolume removes a volume from the specified machine.
|
||||
func (cli *Client) RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error {
|
||||
machine, err := cli.InspectMachine(ctx, machineNameOrID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect machine '%s': %w", machineNameOrID, err)
|
||||
}
|
||||
// Proxy Docker gRPC requests to the selected machine.
|
||||
ctx = proxyToMachine(ctx, machine.Machine)
|
||||
|
||||
pw := progress.ContextWriter(ctx)
|
||||
eventID := fmt.Sprintf("Volume %s on %s", volumeName, machine.Machine.Name)
|
||||
pw.Event(progress.RemovingEvent(eventID))
|
||||
|
||||
if err = cli.Docker.RemoveVolume(ctx, volumeName, force); err != nil {
|
||||
if dockerclient.IsErrNotFound(err) {
|
||||
return api.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("remove volume '%s' from machine '%s': %w",
|
||||
volumeName, machine.Machine.Name, err)
|
||||
}
|
||||
pw.Event(progress.RemovedEvent(eventID))
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user