internal/stack/compose.go (193 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package stack
import (
"context"
"fmt"
"strings"
"github.com/elastic/elastic-package/internal/compose"
"github.com/elastic/elastic-package/internal/docker"
"github.com/elastic/elastic-package/internal/install"
)
type ServiceStatus struct {
Name string
Status string
Version string
}
const readyServicesSuffix = "is_ready"
const (
// projectLabelDockerCompose is the label with the project name created by docker-compose
projectLabelDockerCompose = "com.docker.compose.project"
)
type envBuilder struct {
vars []string
}
// TODO: Use template variables instead of environment variables to parameterize docker-compose.
func newEnvBuilder() *envBuilder {
return new(envBuilder)
}
func (eb *envBuilder) withEnvs(envs []string) *envBuilder {
eb.vars = append(eb.vars, envs...)
return eb
}
func (eb *envBuilder) withEnv(env string) *envBuilder {
eb.vars = append(eb.vars, env)
return eb
}
func (eb *envBuilder) build() []string {
return eb.vars
}
func dockerComposeBuild(ctx context.Context, options Options) error {
c, err := compose.NewProject(DockerComposeProjectName(options.Profile), options.Profile.Path(ProfileStackPath, ComposeFile))
if err != nil {
return fmt.Errorf("could not create docker compose project: %w", err)
}
appConfig, err := install.Configuration(install.OptionWithStackVersion(options.StackVersion))
if err != nil {
return fmt.Errorf("can't read application configuration: %w", err)
}
opts := compose.CommandOptions{
Env: newEnvBuilder().
withEnvs(appConfig.StackImageRefs().AsEnv()).
withEnv(stackVariantAsEnv(options.StackVersion)).
withEnvs(options.Profile.ComposeEnvVars()).
build(),
Services: withIsReadyServices(withDependentServices(options.Services)),
}
if err := c.Build(ctx, opts); err != nil {
return fmt.Errorf("running command failed: %w", err)
}
return nil
}
func dockerComposePull(ctx context.Context, options Options) error {
c, err := compose.NewProject(DockerComposeProjectName(options.Profile), options.Profile.Path(ProfileStackPath, ComposeFile))
if err != nil {
return fmt.Errorf("could not create docker compose project: %w", err)
}
appConfig, err := install.Configuration(install.OptionWithStackVersion(options.StackVersion))
if err != nil {
return fmt.Errorf("can't read application configuration: %w", err)
}
opts := compose.CommandOptions{
Env: newEnvBuilder().
withEnvs(appConfig.StackImageRefs().AsEnv()).
withEnv(stackVariantAsEnv(options.StackVersion)).
withEnvs(options.Profile.ComposeEnvVars()).
build(),
Services: withIsReadyServices(withDependentServices(options.Services)),
}
if err := c.Pull(ctx, opts); err != nil {
return fmt.Errorf("running command failed: %w", err)
}
return nil
}
func dockerComposeUp(ctx context.Context, options Options) error {
c, err := compose.NewProject(DockerComposeProjectName(options.Profile), options.Profile.Path(ProfileStackPath, ComposeFile))
if err != nil {
return fmt.Errorf("could not create docker compose project: %w", err)
}
var args []string
if options.DaemonMode {
args = append(args, "-d")
}
appConfig, err := install.Configuration(install.OptionWithStackVersion(options.StackVersion))
if err != nil {
return fmt.Errorf("can't read application configuration: %w", err)
}
opts := compose.CommandOptions{
Env: newEnvBuilder().
withEnvs(appConfig.StackImageRefs().AsEnv()).
withEnv(stackVariantAsEnv(options.StackVersion)).
withEnvs(options.Profile.ComposeEnvVars()).
build(),
ExtraArgs: args,
Services: withIsReadyServices(withDependentServices(options.Services)),
}
if err := c.Up(ctx, opts); err != nil {
return fmt.Errorf("running command failed: %w", err)
}
return nil
}
func dockerComposeDown(ctx context.Context, options Options) error {
c, err := compose.NewProject(DockerComposeProjectName(options.Profile), options.Profile.Path(ProfileStackPath, ComposeFile))
if err != nil {
return fmt.Errorf("could not create docker compose project: %w", err)
}
appConfig, err := install.Configuration(install.OptionWithStackVersion(options.StackVersion))
if err != nil {
return fmt.Errorf("can't read application configuration: %w", err)
}
downOptions := compose.CommandOptions{
Env: newEnvBuilder().
withEnvs(appConfig.StackImageRefs().AsEnv()).
withEnv(stackVariantAsEnv(options.StackVersion)).
withEnvs(options.Profile.ComposeEnvVars()).
build(),
// Remove associated volumes.
ExtraArgs: []string{"--volumes", "--remove-orphans"},
}
if err := c.Down(ctx, downOptions); err != nil {
return fmt.Errorf("running command failed: %w", err)
}
return nil
}
func withDependentServices(services []string) []string {
for _, aService := range services {
if aService == "elastic-agent" {
return []string{} // elastic-agent service requires to load all other services
}
}
return services
}
func withIsReadyServices(services []string) []string {
if len(services) == 0 {
return services // load all defined services
}
var allServices []string
for _, aService := range services {
allServices = append(allServices, aService, fmt.Sprintf("%s_%s", aService, readyServicesSuffix))
}
return allServices
}
func dockerComposeStatus(ctx context.Context, options Options) ([]ServiceStatus, error) {
var services []ServiceStatus
// query directly to docker to avoid load environment variables (e.g. STACK_VERSION_VARIANT) and profiles
containerIDs, err := docker.ContainerIDsWithLabel(projectLabelDockerCompose, DockerComposeProjectName(options.Profile))
if err != nil {
return nil, err
}
if len(containerIDs) == 0 {
return services, nil
}
containerDescriptions, err := docker.InspectContainers(containerIDs...)
if err != nil {
return nil, err
}
for _, containerDescription := range containerDescriptions {
service, err := newServiceStatus(&containerDescription)
if err != nil {
return nil, err
}
services = append(services, *service)
}
return services, nil
}
func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) {
service := ServiceStatus{
Name: description.Config.Labels.ComposeService,
Status: description.State.Status,
Version: getVersionFromDockerImage(description.Config.Image),
}
if description.State.Status == "running" {
healthStatus := "unknown health"
if health := description.State.Health; health != nil {
healthStatus = health.Status
}
service.Status = fmt.Sprintf("%v (%v)", service.Status, healthStatus)
}
if description.State.Status == "exited" {
service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.ExitCode)
}
return &service, nil
}
func getVersionFromDockerImage(dockerImage string) string {
_, version, found := strings.Cut(dockerImage, ":")
if found {
return version
}
return "latest"
}