diff --git a/internal/cli/client/compose.go b/internal/cli/client/compose.go index 35d2815a..911241d5 100644 --- a/internal/cli/client/compose.go +++ b/internal/cli/client/compose.go @@ -1,14 +1,43 @@ package client -import "github.com/compose-spec/compose-go/v2/types" +import ( + "context" + "fmt" + "github.com/compose-spec/compose-go/v2/graph" + "github.com/compose-spec/compose-go/v2/types" + "uncloud/internal/compose" +) -type ComposeDeployment struct { - project *types.Project +func PlanComposeDeployment(ctx context.Context, project *types.Project, cli *Client) (SequenceOperation, error) { + plan := SequenceOperation{} + err := graph.InDependencyOrder(ctx, project, + func(ctx context.Context, name string, service types.ServiceConfig) error { + spec, err := compose.ServiceSpecFromCompose(name, service) + if err != nil { + return fmt.Errorf("convert compose service '%s' to service spec: %w", name, err) + } + if spec, err = cli.PrepareDeploymentSpec(ctx, spec); err != nil { + return fmt.Errorf("prepare service '%s' spec ready for deployment: %w", name, err) + } + + // TODO: properly handle dependency conditions in the service deployment plan as the first operation. + deploy, err := cli.NewDeployment(spec, nil) + if err != nil { + return fmt.Errorf("create deployment for service '%s': %w", name, err) + } + + servicePlan, err := deploy.Plan(ctx) + if err != nil { + return fmt.Errorf("create deployment plan for service '%s': %w", name, err) + } + + // Skip no-op (up-to-date) service plans. + if len(servicePlan.Operations) > 0 { + plan.Operations = append(plan.Operations, &servicePlan) + } + + return nil + }) + + return plan, err } - -func NewComposeDeployment(project *types.Project) *ComposeDeployment { - return &ComposeDeployment{project: project} -} - -// TODO: implement Plan method to generate a deployment plan that consists of a sequence of operations with -// service Deployments. diff --git a/internal/compose/port.go b/internal/compose/port.go index ff2b0108..9d998c16 100644 --- a/internal/compose/port.go +++ b/internal/compose/port.go @@ -12,23 +12,21 @@ type PortsSource []string // TransformServicesPortsExtension transforms the ports extension of all services in the project by replacing a string // representation of each port with a parsed PortSpec. -func transformServicesPortsExtension(project *types.Project) error { - for name, service := range project.Services { +func transformServicesPortsExtension(project *types.Project) (*types.Project, error) { + return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) { ports, ok := service.Extensions[PortsExtensionKey].(PortsSource) if !ok { - continue + return service, nil } specs, err := transformPortsExtension(ports) if err != nil { - return err + return service, err } service.Extensions[PortsExtensionKey] = specs - project.Services[name] = service - } - - return nil + return service, nil + }) } func transformPortsExtension(ports PortsSource) ([]api.PortSpec, error) { diff --git a/internal/compose/project.go b/internal/compose/project.go index 7e38f298..042145cc 100644 --- a/internal/compose/project.go +++ b/internal/compose/project.go @@ -18,7 +18,7 @@ func LoadProject(ctx context.Context, paths []string) (*types.Project, error) { composecli.WithConfigFileEnv, // If none was selected, get default compose.yaml file from current dir or parent folders. composecli.WithDefaultConfigPath, - composecli.WithExtension("x-ports", PortsSource{}), + composecli.WithExtension(PortsExtensionKey, PortsSource{}), ) if err != nil { return nil, fmt.Errorf("create compose parser options: %w", err) @@ -29,7 +29,7 @@ func LoadProject(ctx context.Context, paths []string) (*types.Project, error) { return nil, err } - if err = transformServicesPortsExtension(project); err != nil { + if project, err = transformServicesPortsExtension(project); err != nil { return nil, err } diff --git a/internal/compose/service.go b/internal/compose/service.go new file mode 100644 index 00000000..0c18541f --- /dev/null +++ b/internal/compose/service.go @@ -0,0 +1,40 @@ +package compose + +import ( + "fmt" + "github.com/compose-spec/compose-go/v2/types" + "uncloud/internal/api" +) + +func ServiceSpecFromCompose(name string, service types.ServiceConfig) (api.ServiceSpec, error) { + spec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Command: service.Command, + Image: service.Image, + Init: service.Init, + // TODO: env + // TODO: volumes + }, + Name: name, + } + + if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok { + spec.Ports = ports + } + + if service.Deploy != nil { + switch service.Deploy.Mode { + case "global": + spec.Mode = api.ServiceModeGlobal + case "", "replicated": + spec.Mode = api.ServiceModeReplicated + if service.Deploy.Replicas != nil { + spec.Replicas = uint(*service.Deploy.Replicas) + } + default: + return spec, fmt.Errorf("unsupported deploy mode: %s", service.Deploy.Mode) + } + } + + return spec, nil +}