mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
chore: replace machine filter with placement constraint in service spec
This commit is contained in:
+12
-30
@@ -19,9 +19,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type deployOptions struct {
|
type deployOptions struct {
|
||||||
image string
|
image string
|
||||||
machine string
|
machines []string
|
||||||
context string
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployCommand() *cobra.Command {
|
func NewDeployCommand() *cobra.Command {
|
||||||
@@ -40,8 +40,9 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().StringVar(&opts.image, "image", "",
|
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||||
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
||||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Machine names to deploy to (comma-separated). (default is all machines)")
|
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||||
|
"list of machine names. (default is all machines)")
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.context, "context", "c", "",
|
&opts.context, "context", "c", "",
|
||||||
"Name of the cluster context to deploy to. (default is the current context)",
|
"Name of the cluster context to deploy to. (default is the current context)",
|
||||||
@@ -88,16 +89,10 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Preparing a deployment plan...")
|
fmt.Println("Preparing a deployment plan...")
|
||||||
|
|
||||||
var filter deploy.MachineFilter
|
placement := api.Placement{
|
||||||
if opts.machine != "" {
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
machines := strings.Split(opts.machine, ",")
|
|
||||||
for i, m := range machines {
|
|
||||||
machines[i] = strings.TrimSpace(m)
|
|
||||||
}
|
|
||||||
filter = machineFilter(machines)
|
|
||||||
}
|
}
|
||||||
|
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
|
||||||
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
@@ -108,31 +103,18 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, deploy.ErrNoMatchingMachines) {
|
|
||||||
return fmt.Errorf("no machines found matching: %s", opts.machine)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("plan caddy deployment: %w", err)
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Operations) == 0 {
|
if len(plan.Operations) == 0 {
|
||||||
if opts.machine != "" {
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if svc.ID == "" {
|
if svc.ID == "" {
|
||||||
if opts.machine != "" {
|
if len(opts.machines) > 0 {
|
||||||
fmt.Println("This will run a Caddy container on selected machines.")
|
fmt.Println("This will run a Caddy container on each selected machine.")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("This will run a Caddy container on each machine.")
|
fmt.Println("This will run a Caddy container on each machine.")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if opts.machine != "" {
|
|
||||||
fmt.Println("This will perform a rolling update of Caddy containers on selected machines.")
|
|
||||||
} else {
|
|
||||||
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialise a machine and container name resolver to properly format the plan output.
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
|
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
"Name of the cluster context to deploy to (default is the current context)")
|
"Name of the cluster context to deploy to (default is the current context)")
|
||||||
|
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
|
||||||
|
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ import (
|
|||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
@@ -105,14 +103,6 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
|
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inspect machine: %w", err)
|
|
||||||
}
|
|
||||||
filter := func(m *pb.MachineInfo) bool {
|
|
||||||
return m.Id == minfo.Id
|
|
||||||
}
|
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
// use the deployed image version. Otherwise, use the latest version.
|
// use the deployed image version. Otherwise, use the latest version.
|
||||||
caddyImage := ""
|
caddyImage := ""
|
||||||
@@ -137,7 +127,9 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
|
d, err := machineClient.NewCaddyDeployment(caddyImage, api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.noCaddy {
|
if !opts.noCaddy {
|
||||||
d, err := client.NewCaddyDeployment("", nil)
|
d, err := client.NewCaddyDeployment("", api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-33
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/docker/docker/daemon/names"
|
"github.com/docker/docker/daemon/names"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
@@ -66,7 +65,7 @@ func NewRunCommand() *cobra.Command {
|
|||||||
"the machines) or '%s' (one container on every machine).",
|
"the machines) or '%s' (one container on every machine).",
|
||||||
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
||||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Placement constraint by machine name, limiting which machines the service can run on. Can be specified "+
|
"Placement constraint by machine names, limiting which machines the service can run on. Can be specified "+
|
||||||
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||||
"Assign a name to the service. A random name is generated if not specified.")
|
"Assign a name to the service. A random name is generated if not specified.")
|
||||||
@@ -114,26 +113,15 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
defer clusterClient.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
var deployFilter deploy.MachineFilter
|
machineIDForVolumes, missingVolumes, err := selectMachineForVolumes(
|
||||||
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
ctx,
|
||||||
if len(machines) > 0 {
|
clusterClient,
|
||||||
deployFilter = func(m *pb.MachineInfo) bool {
|
spec.Volumes,
|
||||||
return slices.Contains(machines, m.Name)
|
spec.Placement.Machines,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
machineIDForVolumes, missingVolumes, err := selectMachineForVolumes(ctx, clusterClient, spec.Volumes, machines)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// machineIDForVolumes is not empty if the spec includes named volumes.
|
|
||||||
if machineIDForVolumes != "" {
|
|
||||||
// The service must be deployed on the machine where the existing volumes are located and the missing ones
|
|
||||||
// will be created.
|
|
||||||
deployFilter = func(m *pb.MachineInfo) bool {
|
|
||||||
return m.Id == machineIDForVolumes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp client.RunServiceResponse
|
var resp client.RunServiceResponse
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
@@ -145,7 +133,7 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = clusterClient.RunService(ctx, spec, deployFilter)
|
resp, err = clusterClient.RunService(ctx, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run service: %w", err)
|
return fmt.Errorf("run service: %w", err)
|
||||||
}
|
}
|
||||||
@@ -207,6 +195,10 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
return spec, err
|
return spec, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
placement := api.Placement{
|
||||||
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
|
}
|
||||||
|
|
||||||
spec = api.ServiceSpec{
|
spec = api.ServiceSpec{
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: opts.command,
|
Command: opts.command,
|
||||||
@@ -215,11 +207,12 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
PullPolicy: opts.pull,
|
PullPolicy: opts.pull,
|
||||||
VolumeMounts: mounts,
|
VolumeMounts: mounts,
|
||||||
},
|
},
|
||||||
Mode: opts.mode,
|
Mode: opts.mode,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
Ports: ports,
|
Placement: placement,
|
||||||
Replicas: opts.replicas,
|
Ports: ports,
|
||||||
Volumes: volumes,
|
Replicas: opts.replicas,
|
||||||
|
Volumes: volumes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
||||||
@@ -372,9 +365,9 @@ func selectMachineForVolumes(
|
|||||||
ctx context.Context, clusterClient *client.Client, volumes []api.VolumeSpec, machinesFilter []string,
|
ctx context.Context, clusterClient *client.Client, volumes []api.VolumeSpec, machinesFilter []string,
|
||||||
) (machineID string, missingVolumes []api.VolumeSpec, err error) {
|
) (machineID string, missingVolumes []api.VolumeSpec, err error) {
|
||||||
var volumeNames []string
|
var volumeNames []string
|
||||||
for _, volume := range volumes {
|
for _, v := range volumes {
|
||||||
if volume.Type == api.VolumeTypeVolume {
|
if v.Type == api.VolumeTypeVolume {
|
||||||
volumeNames = append(volumeNames, volume.Name)
|
volumeNames = append(volumeNames, v.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(volumeNames) == 0 {
|
if len(volumeNames) == 0 {
|
||||||
@@ -424,15 +417,15 @@ func selectMachineForVolumes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find missing volumes that need to be created on the selected machine.
|
// Find missing volumes that need to be created on the selected machine.
|
||||||
for _, volume := range volumes {
|
for _, v := range volumes {
|
||||||
if volume.Type != api.VolumeTypeVolume {
|
if v.Type != api.VolumeTypeVolume {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.ContainsFunc(vols, func(v api.MachineVolume) bool {
|
if !slices.ContainsFunc(vols, func(mv api.MachineVolume) bool {
|
||||||
return v.Volume.Name == volume.Name && v.MachineID == machineID
|
return mv.Volume.Name == v.Name && mv.MachineID == machineID
|
||||||
}) {
|
}) {
|
||||||
missingVolumes = append(missingVolumes, volume)
|
missingVolumes = append(missingVolumes, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
// Placement defines the placement constraints for service containers.
|
||||||
|
type Placement struct {
|
||||||
|
// Machines is a list of machine names or IDs where service containers are allowed to be deployed.
|
||||||
|
// If empty, containers can be deployed to any available machine in the cluster.
|
||||||
|
Machines []string `json:",omitempty"`
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ type ServiceSpec struct {
|
|||||||
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
||||||
Mode string
|
Mode string
|
||||||
Name string
|
Name string
|
||||||
|
// Placement defines the placement constraints for the service.
|
||||||
|
Placement Placement
|
||||||
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
||||||
Ports []PortSpec
|
Ports []PortSpec
|
||||||
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
||||||
|
|||||||
+22
-6
@@ -2,13 +2,14 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -22,7 +23,7 @@ var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
|
|||||||
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service.
|
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service.
|
||||||
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
|
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
|
||||||
// version of the official Caddy Docker image is used.
|
// version of the official Caddy Docker image is used.
|
||||||
func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter) (*deploy.Deployment, error) {
|
func (cli *Client) NewCaddyDeployment(image string, placement api.Placement) (*deploy.Deployment, error) {
|
||||||
latest, err := LatestCaddyImage()
|
latest, err := LatestCaddyImage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
||||||
@@ -36,10 +37,16 @@ func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter)
|
|||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
|
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
|
||||||
Image: image,
|
Image: image,
|
||||||
Volumes: []string{"/var/lib/uncloud/caddy:/config"},
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "config",
|
||||||
|
ContainerPath: "/config",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Mode: api.ServiceModeGlobal,
|
Mode: api.ServiceModeGlobal,
|
||||||
Name: CaddyServiceName,
|
Name: CaddyServiceName,
|
||||||
|
Placement: placement,
|
||||||
Ports: []api.PortSpec{
|
Ports: []api.PortSpec{
|
||||||
{
|
{
|
||||||
PublishedPort: 80,
|
PublishedPort: 80,
|
||||||
@@ -54,9 +61,18 @@ func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter)
|
|||||||
Mode: api.PortModeHost,
|
Mode: api.PortModeHost,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: "/var/lib/uncloud/caddy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.NewDeployment(spec, &deploy.RollingStrategy{MachineFilter: filter}), nil
|
return cli.NewDeployment(spec, nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
|
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Client interface {
|
|||||||
api.ImageClient
|
api.ImageClient
|
||||||
api.MachineClient
|
api.MachineClient
|
||||||
api.ServiceClient
|
api.ServiceClient
|
||||||
|
api.VolumeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deployment manages the process of creating or updating a service to match a desired state.
|
// Deployment manages the process of creating or updating a service to match a desired state.
|
||||||
|
|||||||
@@ -16,10 +16,22 @@ type Constraint interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func constraintsFromSpec(spec api.ServiceSpec) []Constraint {
|
func constraintsFromSpec(spec api.ServiceSpec) []Constraint {
|
||||||
return []Constraint{}
|
var constraints []Constraint
|
||||||
|
|
||||||
|
if len(spec.Placement.Machines) > 0 {
|
||||||
|
constraints = append(constraints, &PlacementConstraint{
|
||||||
|
Machines: spec.Placement.Machines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: inspect and add VolumeConstraint.
|
||||||
|
|
||||||
|
return constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlacementConstraint struct {
|
type PlacementConstraint struct {
|
||||||
|
// Machines is a list of machine names or IDs where service containers are allowed to be deployed.
|
||||||
|
// If empty, containers can be deployed to any available machine in the cluster.
|
||||||
Machines []string
|
Machines []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,38 @@
|
|||||||
package scheduler
|
package scheduler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServiceScheduler struct {
|
type ServiceScheduler struct {
|
||||||
machines []*Machine
|
Machines []*Machine
|
||||||
spec api.ServiceSpec
|
Spec api.ServiceSpec
|
||||||
constraints []Constraint
|
Constraints []Constraint
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceScheduler(machines []*Machine, spec api.ServiceSpec, constraints []Constraint) *ServiceScheduler {
|
func NewServiceScheduler(ctx context.Context, cli Client, spec api.ServiceSpec) (*ServiceScheduler, error) {
|
||||||
specConstraints := constraintsFromSpec(spec)
|
machines, err := InspectMachines(ctx, cli)
|
||||||
specConstraints = append(specConstraints, constraints...)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
constraints := constraintsFromSpec(spec)
|
||||||
|
|
||||||
return &ServiceScheduler{
|
return &ServiceScheduler{
|
||||||
machines: machines,
|
Machines: machines,
|
||||||
spec: spec,
|
Spec: spec,
|
||||||
constraints: specConstraints,
|
Constraints: constraints,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceScheduler) AvailableMachines() ([]*Machine, error) {
|
func (s *ServiceScheduler) AvailableMachines() ([]*Machine, error) {
|
||||||
var available []*Machine
|
var available []*Machine
|
||||||
for _, machine := range s.machines {
|
for _, machine := range s.Machines {
|
||||||
if s.evaluateConstraints(machine) {
|
if s.evaluateConstraints(machine) {
|
||||||
available = append(available, machine)
|
available = append(available, machine)
|
||||||
}
|
}
|
||||||
@@ -38,7 +44,7 @@ func (s *ServiceScheduler) AvailableMachines() ([]*Machine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceScheduler) evaluateConstraints(machine *Machine) bool {
|
func (s *ServiceScheduler) evaluateConstraints(machine *Machine) bool {
|
||||||
for _, c := range s.constraints {
|
for _, c := range s.Constraints {
|
||||||
if !c.Evaluate(machine) {
|
if !c.Evaluate(machine) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
|
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
|
||||||
@@ -18,22 +19,19 @@ type Strategy interface {
|
|||||||
Type() string
|
Type() string
|
||||||
// Plan returns the operation to reconcile the service to the desired state.
|
// Plan returns the operation to reconcile the service to the desired state.
|
||||||
// If the service does not exist (new deployment), svc will be nil.
|
// If the service does not exist (new deployment), svc will be nil.
|
||||||
Plan(ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec) (Plan, error)
|
Plan(ctx context.Context, cli scheduler.Client, svc *api.Service, spec api.ServiceSpec) (Plan, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
|
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
|
||||||
// to minimize service disruption.
|
// to minimize service disruption.
|
||||||
type RollingStrategy struct {
|
type RollingStrategy struct{}
|
||||||
// MachineFilter optionally restricts which machines can be used for deployment.
|
|
||||||
MachineFilter MachineFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RollingStrategy) Type() string {
|
func (s *RollingStrategy) Type() string {
|
||||||
return "rolling"
|
return "rolling"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RollingStrategy) Plan(
|
func (s *RollingStrategy) Plan(
|
||||||
ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec,
|
ctx context.Context, cli scheduler.Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
) (Plan, error) {
|
) (Plan, error) {
|
||||||
// We can assume that the spec is valid at this point because it has been validated by the deployment.
|
// We can assume that the spec is valid at this point because it has been validated by the deployment.
|
||||||
switch spec.Mode {
|
switch spec.Mode {
|
||||||
@@ -51,37 +49,29 @@ func (s *RollingStrategy) Plan(
|
|||||||
// in the cluster.
|
// in the cluster.
|
||||||
// TODO: schedule containers only on machines that contain the image if pull policy is set to 'never'.
|
// TODO: schedule containers only on machines that contain the image if pull policy is set to 'never'.
|
||||||
func (s *RollingStrategy) planReplicated(
|
func (s *RollingStrategy) planReplicated(
|
||||||
ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec,
|
ctx context.Context, cli scheduler.Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
) (Plan, error) {
|
) (Plan, error) {
|
||||||
plan, err := newEmptyPlan(svc, spec)
|
plan, err := newEmptyPlan(svc, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
}
|
}
|
||||||
|
|
||||||
availableMachines, err := cli.ListMachines(ctx, &api.MachineFilter{Available: true})
|
sched, err := scheduler.NewServiceScheduler(ctx, cli, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, fmt.Errorf("list machines: %w", err)
|
return plan, err
|
||||||
|
}
|
||||||
|
// TODO: return a detailed report on required constraints and which ones are satisfied?
|
||||||
|
availableMachines, err := sched.AvailableMachines()
|
||||||
|
if err != nil {
|
||||||
|
return plan, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter machines that match the machine filter if provided.
|
|
||||||
var matchedMachines []*pb.MachineInfo
|
var matchedMachines []*pb.MachineInfo
|
||||||
var unmatchedMachines []*pb.MachineInfo
|
|
||||||
for _, m := range availableMachines {
|
for _, m := range availableMachines {
|
||||||
if s.MachineFilter == nil || s.MachineFilter(m.Machine) {
|
matchedMachines = append(matchedMachines, m.Info)
|
||||||
matchedMachines = append(matchedMachines, m.Machine)
|
|
||||||
} else {
|
|
||||||
unmatchedMachines = append(unmatchedMachines, m.Machine)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(matchedMachines) == 0 {
|
// TODO: filter machines that contain the service volumes if the service uses any.
|
||||||
if s.MachineFilter != nil {
|
|
||||||
return plan, ErrNoMatchingMachines
|
|
||||||
}
|
|
||||||
return plan, fmt.Errorf("no available machines to deploy service")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: filter machines that contain the service volumes if the service uses any.s
|
|
||||||
|
|
||||||
// Randomise the order of machines to avoid always deploying to the same machines first.
|
// Randomise the order of machines to avoid always deploying to the same machines first.
|
||||||
rand.Shuffle(len(matchedMachines), func(i, j int) {
|
rand.Shuffle(len(matchedMachines), func(i, j int) {
|
||||||
@@ -209,7 +199,7 @@ func (s *RollingStrategy) planReplicated(
|
|||||||
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
|
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
|
||||||
// that are down.
|
// that are down.
|
||||||
func (s *RollingStrategy) planGlobal(
|
func (s *RollingStrategy) planGlobal(
|
||||||
ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec,
|
ctx context.Context, cli scheduler.Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
) (Plan, error) {
|
) (Plan, error) {
|
||||||
plan, err := newEmptyPlan(svc, spec)
|
plan, err := newEmptyPlan(svc, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,39 +216,36 @@ func (s *RollingStrategy) planGlobal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx, nil)
|
sched, err := scheduler.NewServiceScheduler(ctx, cli, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, fmt.Errorf("list machines: %w", err)
|
return plan, err
|
||||||
}
|
|
||||||
// Filter machines if a machine filter is provided.
|
|
||||||
// TODO: not sure this is the right behaviour to ignore other machines that might run service containers.
|
|
||||||
// Maybe there should be another filter to specify which machines to deploy to but keep the rest running.
|
|
||||||
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
|
||||||
if s.MachineFilter != nil {
|
|
||||||
machines = slices.DeleteFunc(machines, func(m *pb.MachineMember) bool {
|
|
||||||
return !s.MachineFilter(m.Machine)
|
|
||||||
})
|
|
||||||
if len(machines) == 0 {
|
|
||||||
return plan, ErrNoMatchingMachines
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: figure out how to return a warning if there are machines down. Embed the machinesDown in the plan?
|
availableMachines, err := sched.AvailableMachines()
|
||||||
var machinesDown []*pb.MachineInfo
|
if err != nil {
|
||||||
for _, m := range machines {
|
return plan, err
|
||||||
// Skip machines that are down but collect them to report a warning later.
|
}
|
||||||
if m.State == pb.MachineMember_DOWN {
|
|
||||||
machinesDown = append(machinesDown, m.Machine)
|
|
||||||
fmt.Printf("WARNING: failed to run a service container on machine '%s' which is Down.\n", m.Machine.Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
containers := containersOnMachine[m.Machine.Id]
|
for _, m := range availableMachines {
|
||||||
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Machine.Id)
|
containers := containersOnMachine[m.Info.Id]
|
||||||
|
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Info.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
}
|
}
|
||||||
plan.Operations = append(plan.Operations, ops...)
|
plan.Operations = append(plan.Operations, ops...)
|
||||||
|
|
||||||
|
delete(containersOnMachine, m.Info.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any remaining containers on machines that don't match the new placement constraints.
|
||||||
|
for _, containers := range containersOnMachine {
|
||||||
|
for _, c := range containers {
|
||||||
|
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
ContainerID: c.Container.ID,
|
||||||
|
MachineID: c.MachineID,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan, nil
|
return plan, nil
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ type RunServiceResponse struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) RunService(
|
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
|
||||||
ctx context.Context, spec api.ServiceSpec, filter deploy.MachineFilter,
|
|
||||||
) (RunServiceResponse, error) {
|
|
||||||
var resp RunServiceResponse
|
var resp RunServiceResponse
|
||||||
|
|
||||||
if err := spec.Validate(); err != nil {
|
if err := spec.Validate(); err != nil {
|
||||||
@@ -42,7 +40,7 @@ func (cli *Client) RunService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deployment := cli.NewDeployment(spec, &deploy.RollingStrategy{MachineFilter: filter})
|
deployment := cli.NewDeployment(spec, &deploy.RollingStrategy{})
|
||||||
plan, err := deployment.Run(ctx)
|
plan, err := deployment.Run(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
|
|||||||
+3
-3
@@ -114,7 +114,7 @@ func sortMounts(mounts []mount.Mount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// serviceContainersByMachine returns a map of machine ID to service containers on that machine.
|
// serviceContainersByMachine returns a map of machine ID to service containers on that machine.
|
||||||
func serviceContainersByMachine(t *testing.T, svc api.Service) map[string][]api.ServiceContainer {
|
func serviceContainersByMachine(svc api.Service) map[string][]api.ServiceContainer {
|
||||||
containers := make(map[string][]api.ServiceContainer)
|
containers := make(map[string][]api.ServiceContainer)
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
containers[c.MachineID] = append(containers[c.MachineID], c.Container)
|
containers[c.MachineID] = append(containers[c.MachineID], c.Container)
|
||||||
@@ -122,7 +122,7 @@ func serviceContainersByMachine(t *testing.T, svc api.Service) map[string][]api.
|
|||||||
return containers
|
return containers
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceMachines(t *testing.T, svc api.Service) mapset.Set[string] {
|
func serviceMachines(svc api.Service) mapset.Set[string] {
|
||||||
machines := mapset.NewSet[string]()
|
machines := mapset.NewSet[string]()
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
machines.Add(c.MachineID)
|
machines.Add(c.MachineID)
|
||||||
@@ -131,7 +131,7 @@ func serviceMachines(t *testing.T, svc api.Service) mapset.Set[string] {
|
|||||||
return machines
|
return machines
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceContainerIDs(t *testing.T, svc api.Service) mapset.Set[string] {
|
func serviceContainerIDs(svc api.Service) mapset.Set[string] {
|
||||||
ids := mapset.NewSet[string]()
|
ids := mapset.NewSet[string]()
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
ids.Add(c.Container.ID)
|
ids.Add(c.Container.ID)
|
||||||
|
|||||||
+87
-143
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
@@ -80,11 +79,11 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, spec)
|
assertServiceMatchesSpec(t, svc, spec)
|
||||||
|
|
||||||
assert.Len(t, svc.Containers, 3)
|
assert.Len(t, svc.Containers, 3)
|
||||||
machines := serviceMachines(t, svc)
|
machines := serviceMachines(svc)
|
||||||
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
||||||
|
|
||||||
// Deploy a published port.
|
// Deploy a published port.
|
||||||
initialContainers := serviceContainerIDs(t, svc)
|
initialContainers := serviceContainerIDs(svc)
|
||||||
|
|
||||||
specWithPort := api.ServiceSpec{
|
specWithPort := api.ServiceSpec{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -115,9 +114,9 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, specWithPort)
|
assertServiceMatchesSpec(t, svc, specWithPort)
|
||||||
|
|
||||||
assert.Len(t, svc.Containers, 3)
|
assert.Len(t, svc.Containers, 3)
|
||||||
machines = serviceMachines(t, svc)
|
machines = serviceMachines(svc)
|
||||||
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
||||||
containers := serviceContainerIDs(t, svc)
|
containers := serviceContainerIDs(svc)
|
||||||
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
||||||
"All existing containers should be replaced")
|
"All existing containers should be replaced")
|
||||||
|
|
||||||
@@ -155,9 +154,9 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, specWithPortAndInit)
|
assertServiceMatchesSpec(t, svc, specWithPortAndInit)
|
||||||
|
|
||||||
assert.Len(t, svc.Containers, 3)
|
assert.Len(t, svc.Containers, 3)
|
||||||
machines = serviceMachines(t, svc)
|
machines = serviceMachines(svc)
|
||||||
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
||||||
containers = serviceContainerIDs(t, svc)
|
containers = serviceContainerIDs(svc)
|
||||||
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
||||||
"All existing containers should be replaced")
|
"All existing containers should be replaced")
|
||||||
|
|
||||||
@@ -176,14 +175,14 @@ func TestDeployment(t *testing.T) {
|
|||||||
svc, err = cli.InspectService(ctx, name)
|
svc, err = cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
containers = serviceContainerIDs(t, svc)
|
containers = serviceContainerIDs(svc)
|
||||||
assert.ElementsMatch(t, initialContainers.ToSlice(), containers.ToSlice())
|
assert.ElementsMatch(t, initialContainers.ToSlice(), containers.ToSlice())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("global with machine filter", func(t *testing.T) {
|
t.Run("global with machine placement", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "global-deployment-filtered"
|
name := "test-global-deployment-machine-placement"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
@@ -191,13 +190,16 @@ func TestDeployment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// First deploy globally without filter to get containers on all machines.
|
// First deploy globally to machines #0 and #1.
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeGlobal,
|
Mode: api.ServiceModeGlobal,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Image: "portainer/pause:latest",
|
Image: "portainer/pause:latest",
|
||||||
},
|
},
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{c.Machines[0].Name, c.Machines[1].Name},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
deployment := cli.NewDeployment(spec, nil)
|
deployment := cli.NewDeployment(spec, nil)
|
||||||
|
|
||||||
@@ -206,89 +208,57 @@ func TestDeployment(t *testing.T) {
|
|||||||
|
|
||||||
svc, err := cli.InspectService(ctx, name)
|
svc, err := cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, svc.Containers, 3, "expected 1 container on each machine")
|
assertServiceMatchesSpec(t, svc, spec)
|
||||||
|
|
||||||
// Store initial container IDs by machine.
|
assert.Len(t, svc.Containers, 2, "Expected 1 container on machines %s and %s",
|
||||||
initialContainers := make(map[string]string) // machineID -> containerID
|
c.Machines[0].Name, c.Machines[1].Name)
|
||||||
for _, ctr := range svc.Containers {
|
initialMachines := serviceMachines(svc)
|
||||||
initialContainers[ctr.MachineID] = ctr.Container.ID
|
assert.ElementsMatch(t, initialMachines.ToSlice(), []string{c.Machines[0].ID, c.Machines[1].ID})
|
||||||
}
|
|
||||||
|
initialContainers := serviceContainerIDs(svc)
|
||||||
|
|
||||||
// Update spec with Init=true, but only deploy to machines #0 and #2.
|
// Update spec with Init=true, but only deploy to machines #0 and #2.
|
||||||
init := true
|
init := true
|
||||||
specWithInit := spec
|
specWithInit := spec
|
||||||
specWithInit.Container.Init = &init
|
specWithInit.Container.Init = &init
|
||||||
|
specWithInit.Placement.Machines = []string{c.Machines[0].Name, c.Machines[2].Name}
|
||||||
|
|
||||||
filter := func(m *pb.MachineInfo) bool {
|
deployment = cli.NewDeployment(specWithInit, nil)
|
||||||
return m.Name == c.Machines[0].Name || m.Name == c.Machines[2].Name
|
|
||||||
}
|
|
||||||
strategy := &deploy.RollingStrategy{MachineFilter: filter}
|
|
||||||
deployment = cli.NewDeployment(specWithInit, strategy)
|
|
||||||
|
|
||||||
_, err = deployment.Run(ctx)
|
_, err = deployment.Run(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
svc, err = cli.InspectService(ctx, name)
|
svc, err = cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, svc.Containers, 3, "still 1 container on each machine")
|
assertServiceMatchesSpec(t, svc, specWithInit)
|
||||||
|
|
||||||
// Verify:
|
assert.Len(t, svc.Containers, 2, "Expected 1 container on machines %s and %s",
|
||||||
// 1. Containers on machines #0 and #2 were updated (new IDs, init enabled)
|
c.Machines[0].Name, c.Machines[2].Name)
|
||||||
// 2. Container on machine #1 remains unchanged (same ID, no init)
|
machines := serviceMachines(svc)
|
||||||
for _, ctr := range svc.Containers {
|
assert.ElementsMatch(t, machines.ToSlice(), []string{c.Machines[0].ID, c.Machines[2].ID})
|
||||||
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
oldContainerID := initialContainers[ctr.MachineID]
|
containers := serviceContainerIDs(svc)
|
||||||
switch machine.Machine.Name {
|
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
||||||
case c.Machines[0].Name, c.Machines[2].Name:
|
"All initial containers should be replaced")
|
||||||
// These containers should be updated.
|
|
||||||
assert.NotEqual(t, oldContainerID, ctr.Container.ID,
|
|
||||||
"Container on machine %s should have been updated", machine.Machine.Name)
|
|
||||||
|
|
||||||
svcSpec := ctr.Container.ServiceSpec
|
// Now deploy the same spec without a placement constraint.
|
||||||
require.NoError(t, err)
|
initialContainers = containers // Reset container tracking.
|
||||||
assert.NotNil(t, svcSpec.Container.Init)
|
specWithInit.Placement = api.Placement{}
|
||||||
assert.True(t, *svcSpec.Container.Init,
|
|
||||||
"Container on machine %s should have init enabled", machine.Machine.Name)
|
|
||||||
case c.Machines[1].Name:
|
|
||||||
// This container should remain unchanged.
|
|
||||||
assert.Equal(t, oldContainerID, ctr.Container.ID,
|
|
||||||
"Container on machine %s should not have been updated", machine.Machine.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now deploy another update without filter - should affect all machines.
|
deployment = cli.NewDeployment(specWithInit, nil)
|
||||||
init = false
|
|
||||||
specWithPort := spec
|
|
||||||
specWithPort.Ports = []api.PortSpec{
|
|
||||||
{
|
|
||||||
PublishedPort: 8001,
|
|
||||||
ContainerPort: 8001,
|
|
||||||
Protocol: api.ProtocolTCP,
|
|
||||||
Mode: api.PortModeHost,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
deployment = cli.NewDeployment(specWithPort, nil)
|
|
||||||
_, err = deployment.Run(ctx)
|
_, err = deployment.Run(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
svc, err = cli.InspectService(ctx, name)
|
svc, err = cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assertServiceMatchesSpec(t, svc, specWithInit)
|
||||||
assert.Len(t, svc.Containers, 3)
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
// Verify all containers are updated with a published port.
|
machines = serviceMachines(svc)
|
||||||
for _, ctr := range svc.Containers {
|
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
||||||
svcSpec := ctr.Container.ServiceSpec
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, svcSpec.Container.Init,
|
|
||||||
"Container on machine %s should have init disabled", ctr.MachineID)
|
|
||||||
|
|
||||||
ports, err := ctr.Container.ServicePorts()
|
containers = serviceContainerIDs(svc)
|
||||||
require.NoError(t, err)
|
assert.True(t, initialContainers.IsSubset(containers), "Expected all initial containers to remain")
|
||||||
assert.Equal(t, specWithPort.Ports, ports,
|
|
||||||
"Container on machine %s should have updated port", ctr.MachineID)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("caddy", func(t *testing.T) {
|
t.Run("caddy", func(t *testing.T) {
|
||||||
@@ -299,7 +269,7 @@ func TestDeployment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
deployment, err := cli.NewCaddyDeployment("", nil)
|
deployment, err := cli.NewCaddyDeployment("", api.Placement{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = deployment.Run(ctx)
|
_, err = deployment.Run(ctx)
|
||||||
@@ -338,7 +308,7 @@ func TestDeployment(t *testing.T) {
|
|||||||
}, ctr.HostConfig.RestartPolicy)
|
}, ctr.HostConfig.RestartPolicy)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("caddy with machine filter", func(t *testing.T) {
|
t.Run("caddy with machine placement", func(t *testing.T) {
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, client.CaddyServiceName)
|
err := cli.RemoveService(ctx, client.CaddyServiceName)
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
@@ -346,12 +316,10 @@ func TestDeployment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Deploy to machine #0
|
// Deploy to machine #0.
|
||||||
filter := func(m *pb.MachineInfo) bool {
|
deployment, err := cli.NewCaddyDeployment("", api.Placement{
|
||||||
return m.Name == c.Machines[0].Name
|
Machines: []string{c.Machines[0].Name},
|
||||||
}
|
})
|
||||||
|
|
||||||
deployment, err := cli.NewCaddyDeployment("", filter)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
image := deployment.Spec.Container.Image
|
image := deployment.Spec.Container.Image
|
||||||
|
|
||||||
@@ -361,17 +329,13 @@ func TestDeployment(t *testing.T) {
|
|||||||
svc, err := cli.InspectService(ctx, client.CaddyServiceName)
|
svc, err := cli.InspectService(ctx, client.CaddyServiceName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, svc.Containers, 1)
|
assert.Len(t, svc.Containers, 1)
|
||||||
ctr0 := svc.Containers[0]
|
assertServiceMatchesSpec(t, svc, deployment.Spec)
|
||||||
|
|
||||||
machine0, err := cli.InspectMachine(ctx, ctr0.MachineID)
|
assert.Equal(t, c.Machines[0].ID, svc.Containers[0].MachineID)
|
||||||
require.NoError(t, err)
|
initialContainerID := svc.Containers[0].Container.ID
|
||||||
assert.Equal(t, c.Machines[0].Name, machine0.Machine.Name)
|
|
||||||
|
|
||||||
// Deploy to machines #0 and #2
|
// Deploy to all machines without a placement constraint.
|
||||||
filter = func(m *pb.MachineInfo) bool {
|
deployment, err = cli.NewCaddyDeployment(image, api.Placement{})
|
||||||
return m.Name == c.Machines[0].Name || m.Name == c.Machines[2].Name
|
|
||||||
}
|
|
||||||
deployment, err = cli.NewCaddyDeployment(image, filter)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = deployment.Run(ctx)
|
_, err = deployment.Run(ctx)
|
||||||
@@ -379,26 +343,20 @@ func TestDeployment(t *testing.T) {
|
|||||||
|
|
||||||
svc, err = cli.InspectService(ctx, client.CaddyServiceName)
|
svc, err = cli.InspectService(ctx, client.CaddyServiceName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, svc.Containers, 2)
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
assertServiceMatchesSpec(t, svc, deployment.Spec)
|
||||||
|
|
||||||
// Existing container ctr0 on machine #0 should be left unchanged.
|
// Initial container on machine #0 should be left unchanged.
|
||||||
var ctr2 api.MachineServiceContainer
|
machines := serviceMachines(svc)
|
||||||
if ctr0.Container.ID == svc.Containers[0].Container.ID {
|
assert.Len(t, machines.ToSlice(), 3, "Expected 1 container on each machine")
|
||||||
ctr2 = svc.Containers[1]
|
containers := serviceContainerIDs(svc)
|
||||||
} else {
|
assert.True(t, containers.Contains(initialContainerID), "Expected initial container to remain")
|
||||||
assert.Equal(t, ctr0.Container.ID, svc.Containers[1].Container.ID)
|
|
||||||
ctr2 = svc.Containers[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
machine2, err := cli.InspectMachine(ctx, ctr2.MachineID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, c.Machines[2].Name, machine2.Machine.Name)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("replicated", func(t *testing.T) {
|
t.Run("replicated", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "replicated-deployment"
|
name := "test-replicated-deployment"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
@@ -436,9 +394,9 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, spec)
|
assertServiceMatchesSpec(t, svc, spec)
|
||||||
|
|
||||||
// Verify containers are on different machines for balanced distribution.
|
// Verify containers are on different machines for balanced distribution.
|
||||||
initialMachines := serviceMachines(t, svc)
|
initialMachines := serviceMachines(svc)
|
||||||
assert.Len(t, initialMachines.ToSlice(), 2, "Expected 2 containers on 2 different machines")
|
assert.Len(t, initialMachines.ToSlice(), 2, "Expected 2 containers on 2 different machines")
|
||||||
initialContainers := serviceContainerIDs(t, svc)
|
initialContainers := serviceContainerIDs(svc)
|
||||||
|
|
||||||
// 2. Update the service with a new configuration.
|
// 2. Update the service with a new configuration.
|
||||||
init := true
|
init := true
|
||||||
@@ -458,10 +416,10 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, updatedSpec)
|
assertServiceMatchesSpec(t, svc, updatedSpec)
|
||||||
|
|
||||||
// Verify containers are on the same machines as before but the initial containers were replaced.
|
// Verify containers are on the same machines as before but the initial containers were replaced.
|
||||||
machines := serviceMachines(t, svc)
|
machines := serviceMachines(svc)
|
||||||
assert.ElementsMatch(t, initialMachines.ToSlice(), machines.ToSlice(),
|
assert.ElementsMatch(t, initialMachines.ToSlice(), machines.ToSlice(),
|
||||||
"Expected containers on the same machines")
|
"Expected containers on the same machines")
|
||||||
containers := serviceContainerIDs(t, svc)
|
containers := serviceContainerIDs(svc)
|
||||||
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
assert.Empty(t, initialContainers.Intersect(containers).ToSlice(),
|
||||||
"All existing containers should be replaced")
|
"All existing containers should be replaced")
|
||||||
|
|
||||||
@@ -485,9 +443,9 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, threeReplicaSpec)
|
assertServiceMatchesSpec(t, svc, threeReplicaSpec)
|
||||||
|
|
||||||
// Verify existing containers remain and a new one was added on a different machine.
|
// Verify existing containers remain and a new one was added on a different machine.
|
||||||
machines = serviceMachines(t, svc)
|
machines = serviceMachines(svc)
|
||||||
assert.Len(t, machines.ToSlice(), 3, "Expected 3 containers on 3 different machines")
|
assert.Len(t, machines.ToSlice(), 3, "Expected 3 containers on 3 different machines")
|
||||||
containers = serviceContainerIDs(t, svc)
|
containers = serviceContainerIDs(svc)
|
||||||
assert.Len(t, containers.Intersect(initialContainers).ToSlice(), 2, "Expected 2 initial containers to remain")
|
assert.Len(t, containers.Intersect(initialContainers).ToSlice(), 2, "Expected 2 initial containers to remain")
|
||||||
|
|
||||||
// 4. Update to 5 replicas with a different configuration.
|
// 4. Update to 5 replicas with a different configuration.
|
||||||
@@ -510,13 +468,13 @@ func TestDeployment(t *testing.T) {
|
|||||||
assertServiceMatchesSpec(t, svc, fourReplicaSpec)
|
assertServiceMatchesSpec(t, svc, fourReplicaSpec)
|
||||||
|
|
||||||
// Verify all existing containers were replaced and new ones are evenly distributed.
|
// Verify all existing containers were replaced and new ones are evenly distributed.
|
||||||
machines = serviceMachines(t, svc)
|
machines = serviceMachines(svc)
|
||||||
assert.Len(t, machines.ToSlice(), 3, "Expected containers on 3 different machines")
|
assert.Len(t, machines.ToSlice(), 3, "Expected containers on 3 different machines")
|
||||||
containers = serviceContainerIDs(t, svc)
|
containers = serviceContainerIDs(svc)
|
||||||
assert.Empty(t, containers.Intersect(initialContainers).ToSlice(),
|
assert.Empty(t, containers.Intersect(initialContainers).ToSlice(),
|
||||||
"All existing containers should be replaced")
|
"All existing containers should be replaced")
|
||||||
|
|
||||||
machineContainers := serviceContainersByMachine(t, svc)
|
machineContainers := serviceContainersByMachine(svc)
|
||||||
for _, ctrs := range machineContainers {
|
for _, ctrs := range machineContainers {
|
||||||
assert.LessOrEqual(t, len(ctrs), 2, "Expected at most 2 containers on each machine")
|
assert.LessOrEqual(t, len(ctrs), 2, "Expected at most 2 containers on each machine")
|
||||||
}
|
}
|
||||||
@@ -536,14 +494,14 @@ func TestDeployment(t *testing.T) {
|
|||||||
svc, err = cli.InspectService(ctx, name)
|
svc, err = cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
containers = serviceContainerIDs(t, svc)
|
containers = serviceContainerIDs(svc)
|
||||||
assert.ElementsMatch(t, initialContainers.ToSlice(), containers.ToSlice())
|
assert.ElementsMatch(t, initialContainers.ToSlice(), containers.ToSlice())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("replicated with machine filter", func(t *testing.T) {
|
t.Run("replicated with machine placement", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "replicated-deployment-filtered"
|
name := "test-replicated-deployment-machine-placement"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
@@ -551,21 +509,20 @@ func TestDeployment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create a replicated service with 2 replicas but limit to machines 0 and 1
|
// Create a replicated service with 2 replicas but limit to machines 0 and 1.
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Image: "portainer/pause:latest",
|
Image: "portainer/pause:latest",
|
||||||
},
|
},
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{c.Machines[0].Name, c.Machines[1].Name},
|
||||||
|
},
|
||||||
Replicas: 2,
|
Replicas: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
machine01Filter := func(m *pb.MachineInfo) bool {
|
deployment := cli.NewDeployment(spec, nil)
|
||||||
return m.Name == c.Machines[0].Name || m.Name == c.Machines[1].Name
|
|
||||||
}
|
|
||||||
strategy := &deploy.RollingStrategy{MachineFilter: machine01Filter}
|
|
||||||
deployment := cli.NewDeployment(spec, strategy)
|
|
||||||
|
|
||||||
_, err = deployment.Run(ctx)
|
_, err = deployment.Run(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -573,27 +530,17 @@ func TestDeployment(t *testing.T) {
|
|||||||
// Verify service has 2 containers on machines 0 and 1.
|
// Verify service has 2 containers on machines 0 and 1.
|
||||||
svc, err := cli.InspectService(ctx, name)
|
svc, err := cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assertServiceMatchesSpec(t, svc, spec)
|
||||||
|
|
||||||
assert.Len(t, svc.Containers, 2)
|
assert.Len(t, svc.Containers, 2)
|
||||||
assert.NotEqual(t, svc.Containers[0].MachineID, svc.Containers[1].MachineID,
|
machines := serviceMachines(svc)
|
||||||
"Expected containers on different machines")
|
assert.ElementsMatch(t, machines.ToSlice(), []string{c.Machines[0].ID, c.Machines[1].ID})
|
||||||
|
|
||||||
machineNames := make(map[string]string)
|
|
||||||
for _, ctr := range svc.Containers {
|
|
||||||
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
machineNames[ctr.MachineID] = machine.Machine.Name
|
|
||||||
|
|
||||||
// Should only be on machines 0 or 1.
|
|
||||||
assert.Contains(t, []string{c.Machines[0].Name, c.Machines[1].Name}, machine.Machine.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now update the filter to only allow machine 2
|
// Now update the filter to only allow machine 2
|
||||||
machine2Filter := func(m *pb.MachineInfo) bool {
|
spec.Placement = api.Placement{
|
||||||
return m.Name == c.Machines[2].Name
|
Machines: []string{c.Machines[2].Name},
|
||||||
}
|
}
|
||||||
strategy = &deploy.RollingStrategy{MachineFilter: machine2Filter}
|
deployment = cli.NewDeployment(spec, nil)
|
||||||
deployment = cli.NewDeployment(spec, strategy)
|
|
||||||
|
|
||||||
_, err = deployment.Run(ctx)
|
_, err = deployment.Run(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -601,14 +548,11 @@ func TestDeployment(t *testing.T) {
|
|||||||
// Verify service now has containers only on machine 2.
|
// Verify service now has containers only on machine 2.
|
||||||
svc, err = cli.InspectService(ctx, name)
|
svc, err = cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assertServiceMatchesSpec(t, svc, spec)
|
||||||
|
|
||||||
assert.Len(t, svc.Containers, 2) // Still 2 replicas.
|
assert.Len(t, svc.Containers, 2) // Still 2 replicas.
|
||||||
assert.Equal(t, svc.Containers[0].MachineID, svc.Containers[1].MachineID,
|
machines = serviceMachines(svc)
|
||||||
"Expected containers on the same machine")
|
assert.Equal(t, machines.ToSlice(), []string{c.Machines[2].ID}, "Expected containers on machine 2 only")
|
||||||
|
|
||||||
machine, err := cli.InspectMachine(ctx, svc.Containers[0].MachineID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, c.Machines[2].Name, machine.Machine.Name, "Containers should only be on machine #2")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Deployments with volumes.
|
// Deployments with volumes.
|
||||||
@@ -880,7 +824,7 @@ func TestServiceLifecycle(t *testing.T) {
|
|||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Image: "portainer/pause:latest",
|
Image: "portainer/pause:latest",
|
||||||
},
|
},
|
||||||
}, nil)
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, resp.ID)
|
assert.NotEmpty(t, resp.ID)
|
||||||
@@ -949,7 +893,7 @@ func TestServiceLifecycle(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
resp, err := cli.RunService(ctx, spec, nil)
|
resp, err := cli.RunService(ctx, spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, resp.ID)
|
svc, err := cli.InspectService(ctx, resp.ID)
|
||||||
@@ -979,7 +923,7 @@ func TestServiceLifecycle(t *testing.T) {
|
|||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Image: "portainer/pause:latest",
|
Image: "portainer/pause:latest",
|
||||||
},
|
},
|
||||||
}, nil)
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, resp.ID)
|
assert.NotEmpty(t, resp.ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user