mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
implement RunService for replicated mode with only 1 replica
This commit is contained in:
+64
-112
@@ -87,46 +87,19 @@ func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunSer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) runGlobalService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
||||||
resp := RunServiceResponse{
|
resp := RunServiceResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: spec.Name,
|
Name: spec.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find a machine to run a service replica on.
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, fmt.Errorf("list machines: %w", err)
|
return resp, fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range machines {
|
// TODO: support selecting a particular machine by ID or name through the user options.
|
||||||
// Run a service container on each available machine.
|
|
||||||
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
|
|
||||||
// TODO: run each machine in a goroutine.
|
|
||||||
createResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
|
||||||
// TODO: collect errors and return after trying all machines.
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("run container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Containers = append(resp.Containers, api.MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: createResp.ID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
return RunServiceResponse{}, fmt.Errorf("replicated mode is not supported yet")
|
|
||||||
//
|
|
||||||
//// Find a machine to run the service on.
|
|
||||||
//machines, err := cli.ListMachines(ctx)
|
|
||||||
//if err != nil {
|
|
||||||
// return resp, fmt.Errorf("list machines: %w", err)
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//var machine *pb.MachineMember
|
//var machine *pb.MachineMember
|
||||||
//if opts.Machine != "" {
|
//if opts.Machine != "" {
|
||||||
// // Check if the machine ID or name exists if it's explicitly specified.
|
// // Check if the machine ID or name exists if it's explicitly specified.
|
||||||
@@ -139,74 +112,24 @@ func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api
|
|||||||
// if machine == nil {
|
// if machine == nil {
|
||||||
// return resp, fmt.Errorf("machine %q not found", opts.Machine)
|
// return resp, fmt.Errorf("machine %q not found", opts.Machine)
|
||||||
// }
|
// }
|
||||||
//} else {
|
|
||||||
// machine, err = firstAvailableMachine(machines)
|
|
||||||
// if err != nil {
|
|
||||||
// return resp, err
|
|
||||||
// }
|
|
||||||
//}
|
//}
|
||||||
//if machine == nil { // This should never happen.
|
|
||||||
// return resp, errors.New("no available machine to run the service")
|
m := firstAvailableMachine(machines)
|
||||||
//}
|
if m == nil {
|
||||||
//
|
return resp, errors.New("no available machine to run the service")
|
||||||
//// Proxy Docker gRPC requests to the selected machine.
|
}
|
||||||
//machineIP, _ := machine.Machine.Network.ManagementIp.ToAddr()
|
|
||||||
//md := metadata.Pairs("machines", machineIP.String())
|
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
||||||
//ctx = metadata.NewOutgoingContext(ctx, md)
|
if err != nil {
|
||||||
//
|
return resp, fmt.Errorf("run container: %w", err)
|
||||||
//serviceID, err := secret.NewID()
|
}
|
||||||
//if err != nil {
|
|
||||||
// return resp, fmt.Errorf("generate service ID: %w", err)
|
resp.Containers = append(resp.Containers, api.MachineContainerID{
|
||||||
//}
|
MachineID: m.Machine.Id,
|
||||||
//
|
ContainerID: runResp.ID,
|
||||||
//serviceName := opts.Name
|
})
|
||||||
//// Generate a random service name if not specified.
|
|
||||||
//if serviceName == "" {
|
return resp, nil
|
||||||
// // Get the image name without the repository and tag/digest parts.
|
|
||||||
// imageName := reference.FamiliarName(image)
|
|
||||||
// // Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
|
||||||
// if i := strings.LastIndex(imageName, "/"); i != -1 {
|
|
||||||
// imageName = imageName[i+1:]
|
|
||||||
// }
|
|
||||||
// // Append a random suffix to the image name to generate an optimistically unique service name.
|
|
||||||
// suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
// if err != nil {
|
|
||||||
// return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
// }
|
|
||||||
// serviceName = fmt.Sprintf("%s-%s", imageName, suffix)
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
//if err != nil {
|
|
||||||
// return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
//}
|
|
||||||
//containerName := fmt.Sprintf("%s-%s", serviceName, suffix)
|
|
||||||
//
|
|
||||||
//config := &container.Config{
|
|
||||||
// Image: opts.Image,
|
|
||||||
// Labels: map[string]string{
|
|
||||||
// service.LabelServiceID: serviceID,
|
|
||||||
// service.LabelServiceName: serviceName,
|
|
||||||
// },
|
|
||||||
//}
|
|
||||||
//netConfig := &network.NetworkingConfig{
|
|
||||||
// EndpointsConfig: map[string]*network.EndpointSettings{
|
|
||||||
// machinedocker.NetworkName: {},
|
|
||||||
// },
|
|
||||||
//}
|
|
||||||
//// TODO: pull image if it doesn't exist on the machine.
|
|
||||||
//createResp, err := cli.CreateContainer(ctx, config, nil, netConfig, nil, containerName)
|
|
||||||
//if err != nil {
|
|
||||||
// return resp, fmt.Errorf("create container: %w", err)
|
|
||||||
//}
|
|
||||||
//if err = cli.StartContainer(ctx, createResp.ID, container.StartOptions{}); err != nil {
|
|
||||||
// return resp, fmt.Errorf("start container: %w", err)
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//resp.ID = serviceID
|
|
||||||
//resp.Name = serviceName
|
|
||||||
//resp.MachineName = machine.Machine.Name
|
|
||||||
//return resp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) runContainer(
|
func (cli *Client) runContainer(
|
||||||
@@ -279,23 +202,52 @@ func (cli *Client) runContainer(
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstAvailableMachine(machines []*pb.MachineMember) (*pb.MachineMember, error) {
|
func (cli *Client) runGlobalService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
||||||
// Find the first UP machine.
|
resp := RunServiceResponse{
|
||||||
upIdx := slices.IndexFunc(machines, func(m *pb.MachineMember) bool {
|
ID: id,
|
||||||
return m.State == pb.MachineMember_UP
|
Name: spec.Name,
|
||||||
})
|
|
||||||
if upIdx != -1 {
|
|
||||||
return machines[upIdx], nil
|
|
||||||
}
|
|
||||||
// There is no UP machine, try to find the first SUSPECT machine.
|
|
||||||
suspectIdx := slices.IndexFunc(machines, func(m *pb.MachineMember) bool {
|
|
||||||
return m.State == pb.MachineMember_SUSPECT
|
|
||||||
})
|
|
||||||
if suspectIdx != -1 {
|
|
||||||
return machines[suspectIdx], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("no available machine to run the service")
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range machines {
|
||||||
|
// Run a service container on each available machine.
|
||||||
|
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
|
||||||
|
// TODO: run each machine in a goroutine.
|
||||||
|
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
||||||
|
// TODO: collect errors and return after trying all machines.
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("run container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Containers = append(resp.Containers, api.MachineContainerID{
|
||||||
|
MachineID: m.Machine.Id,
|
||||||
|
ContainerID: runResp.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstAvailableMachine(machines []*pb.MachineMember) *pb.MachineMember {
|
||||||
|
// Find the first UP machine.
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.State == pb.MachineMember_UP {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// There is no UP machine, try to find the first SUSPECT machine.
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.State == pb.MachineMember_SUSPECT {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InspectService returns detailed information about a service and its containers.
|
// InspectService returns detailed information about a service and its containers.
|
||||||
|
|||||||
@@ -21,6 +21,43 @@ func TestRunService(t *testing.T) {
|
|||||||
cli, err := c.Machines[0].Connect(ctx)
|
cli, err := c.Machines[0].Connect(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("1 replica", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
name := "busybox-1-replica"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if !dockerclient.IsErrNotFound(err) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cli.InspectService(ctx, name)
|
||||||
|
require.ErrorIs(t, err, client.ErrNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := cli.RunService(ctx, api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"sleep", "infinity"},
|
||||||
|
Image: "busybox:latest",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, resp.ID)
|
||||||
|
assert.Equal(t, name, resp.Name)
|
||||||
|
assert.Len(t, resp.Containers, 1)
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, resp.ID, svc.ID)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeReplicated, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 1)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("global mode", func(t *testing.T) {
|
t.Run("global mode", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user