diff --git a/docs/extension.md b/docs/extension.md new file mode 100644 index 000000000..9eeea7b19 --- /dev/null +++ b/docs/extension.md @@ -0,0 +1,104 @@ +# About + +The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of) +application needs, which can interact with other service by relying on network(s). Docker Compose is designed +to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover +many other runtimes, typically cloud services or services natively provided by host. + +The Compose extensibility model has been designed to extend the `service` support to runtimes accessible through +third-party tooling. + +# Architecture + +Compose extensibility relies on the `provider` attribute to select the actual binary responsible for managing +the resource(s) needed to run a service. + +```yaml + database: + provider: + type: awesomecloud + options: + type: mysql + size: 256 +``` + +`provider.type` tells Compose the binary to run, which can be either: +- Another Docker CLI plugin (typically, `model` to run `docker-model`) +- An executable in user's `PATH` + +To be a valid Compose extension, provider command *MUST* accept subcommand `compose` (which can be hidden) +with subcommands `up` and `down`. + +## Up lifecycle + +To execute an application's `up` lifecycle, Compose executes the provider's `compose up` command, passing +the project name, service name and additional options. The `provider.options` are translated +into command line flags. For example: +```console +awesomecloud compose --project-name up --type=mysql --size=256 "database" +``` + +> __Note:__ `project-name` _should_ be used by the provider to tag resources +> set for project, so that later execution with `down` subcommand releases +> all allocated resources set for the project. + +## Communication with Compose + +Providers can interact with Compose using `stdout` as a channel, sending JSON line delimited messages. +JSON messages MUST include a `type` and a `message` attribute. +```json +{ "type": "info", "message": "preparing mysql ..." } +``` + +`type` can be either: +- `info`: Reports status updates to the user. Compose will render message as the service state in the progress UI +- `error`: Lest the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure. +- `setenv`: Let's the plugin tell Compose how dependent services can access the created resource. See next section for further details. + +```mermaid +sequenceDiagram + Shell->>Compose: docker compose up + Compose->>Provider: compose up --project-name=xx --foo=bar "database" + Provider--)Compose: json { "info": "pulling 25%" } + Compose-)Shell: pulling 25% + Provider--)Compose: json { "info": "pulling 50%" } + Compose-)Shell: pulling 50% + Provider--)Compose: json { "info": "pulling 75%" } + Compose-)Shell: pulling 75% + Provider--)Compose: json { "setenv": "URL=http://cloud.com/abcd:1234" } + Compose-)Compose: set DATABASE_URL + Provider-)Compose: EOF (command complete) exit 0 + Compose-)Shell: service started +``` + +## Connection to a service managed by a provider + +A service in the Compose application can declare dependency on a service managed by an external provider: + +```yaml +services: + app: + image: myapp + depends_on: + - database + + database: + provider: + type: awesomecloud +``` + +When the provider command sends a `setenv` JSON message, Compose injects the specified variable into any dependent service, +automatically prefixing it with the service name. For example, if `awesomecloud compose up` returns: +```json +{"type": "setenv", "message": "URL=https://awesomecloud.com/db:1234"} +``` +Then the `app` service, which depends on the service managed by the provider, will receive a `DATABASE_URL` environment variable injected +into its runtime environment. + +> __Note:__ The `compose up` provider command _MUST_ be idempotent. If resource is already running, the command _MUST_ set +> the same environment variables to ensure consistent configuration of dependent services. + +## Down lifecycle + +`down` lifecycle is equivalent to `up` with the ` compose --project-name down ` command. +The provider is responsible for releasing all resources associated with the service. \ No newline at end of file diff --git a/pkg/compose/plugins.go b/pkg/compose/plugins.go index 315f464cd..62ed324d5 100644 --- a/pkg/compose/plugins.go +++ b/pkg/compose/plugins.go @@ -59,7 +59,7 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project, return err } - cmd := s.setupPluginCommand(ctx, project, provider, plugin.Path, command) + cmd := s.setupPluginCommand(ctx, project, service, plugin.Path, command) variables, err := s.executePlugin(ctx, cmd, command, service) if err != nil { @@ -153,11 +153,14 @@ func (s *composeService) getPluginBinaryPath(providerType string) (*manager.Plug return manager.GetPlugin(providerType, s.dockerCli, &cobra.Command{}) } -func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, provider types.ServiceProviderConfig, path, command string) *exec.Cmd { +func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) *exec.Cmd { + provider := *service.Provider + args := []string{"compose", "--project-name", project.Name, command} for k, v := range provider.Options { args = append(args, fmt.Sprintf("--%s=%s", k, v)) } + args = append(args, service.Name) cmd := exec.CommandContext(ctx, path, args...) // Remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone