|
| 1 | +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one |
| 2 | +// or more contributor license agreements. Licensed under the Elastic License; |
| 3 | +// you may not use this file except in compliance with the Elastic License. |
| 4 | + |
| 5 | +package servicedeployer |
| 6 | + |
| 7 | +import ( |
| 8 | + _ "embed" |
| 9 | + "fmt" |
| 10 | + |
| 11 | + "github.com/pkg/errors" |
| 12 | + |
| 13 | + "github.com/elastic/elastic-package/internal/compose" |
| 14 | + "github.com/elastic/elastic-package/internal/configuration/locations" |
| 15 | + "github.com/elastic/elastic-package/internal/docker" |
| 16 | + "github.com/elastic/elastic-package/internal/files" |
| 17 | + "github.com/elastic/elastic-package/internal/install" |
| 18 | + "github.com/elastic/elastic-package/internal/kibana" |
| 19 | + "github.com/elastic/elastic-package/internal/logger" |
| 20 | + "github.com/elastic/elastic-package/internal/stack" |
| 21 | +) |
| 22 | + |
| 23 | +const dockerCustomAgentName = "docker-custom-agent" |
| 24 | + |
| 25 | +// CustomAgentDeployer knows how to deploy a custom elastic-agent defined via |
| 26 | +// a Docker Compose file. |
| 27 | +type CustomAgentDeployer struct { |
| 28 | + cfg string |
| 29 | +} |
| 30 | + |
| 31 | +// NewCustomAgentDeployer returns a new instance of a deployedCustomAgent. |
| 32 | +func NewCustomAgentDeployer(cfgPath string) (*CustomAgentDeployer, error) { |
| 33 | + return &CustomAgentDeployer{ |
| 34 | + cfg: cfgPath, |
| 35 | + }, nil |
| 36 | +} |
| 37 | + |
| 38 | +// SetUp sets up the service and returns any relevant information. |
| 39 | +func (d *CustomAgentDeployer) SetUp(inCtxt ServiceContext) (DeployedService, error) { |
| 40 | + logger.Debug("setting up service using Docker Compose service deployer") |
| 41 | + |
| 42 | + appConfig, err := install.Configuration() |
| 43 | + if err != nil { |
| 44 | + return nil, errors.Wrap(err, "can't read application configuration") |
| 45 | + } |
| 46 | + |
| 47 | + kibanaClient, err := kibana.NewClient() |
| 48 | + if err != nil { |
| 49 | + return nil, errors.Wrap(err, "can't create Kibana client") |
| 50 | + } |
| 51 | + |
| 52 | + stackVersion, err := kibanaClient.Version() |
| 53 | + if err != nil { |
| 54 | + return nil, errors.Wrap(err, "can't read Kibana injected metadata") |
| 55 | + } |
| 56 | + |
| 57 | + env := append( |
| 58 | + appConfig.StackImageRefs(stackVersion).AsEnv(), |
| 59 | + fmt.Sprintf("%s=%s", serviceLogsDirEnv, inCtxt.Logs.Folder.Local), |
| 60 | + ) |
| 61 | + |
| 62 | + ymlPaths, err := d.loadComposeDefinitions() |
| 63 | + if err != nil { |
| 64 | + return nil, err |
| 65 | + } |
| 66 | + |
| 67 | + service := dockerComposeDeployedService{ |
| 68 | + ymlPaths: ymlPaths, |
| 69 | + project: "elastic-package-service", |
| 70 | + sv: ServiceVariant{ |
| 71 | + Name: dockerCustomAgentName, |
| 72 | + Env: env, |
| 73 | + }, |
| 74 | + } |
| 75 | + |
| 76 | + outCtxt := inCtxt |
| 77 | + |
| 78 | + p, err := compose.NewProject(service.project, service.ymlPaths...) |
| 79 | + if err != nil { |
| 80 | + return nil, errors.Wrap(err, "could not create Docker Compose project for service") |
| 81 | + } |
| 82 | + |
| 83 | + // Verify the Elastic stack network |
| 84 | + err = stack.EnsureStackNetworkUp() |
| 85 | + if err != nil { |
| 86 | + return nil, errors.Wrap(err, "Elastic stack network is not ready") |
| 87 | + } |
| 88 | + |
| 89 | + // Clean service logs |
| 90 | + err = files.RemoveContent(outCtxt.Logs.Folder.Local) |
| 91 | + if err != nil { |
| 92 | + return nil, errors.Wrap(err, "removing service logs failed") |
| 93 | + } |
| 94 | + |
| 95 | + inCtxt.Name = dockerCustomAgentName |
| 96 | + serviceName := inCtxt.Name |
| 97 | + opts := compose.CommandOptions{ |
| 98 | + Env: env, |
| 99 | + ExtraArgs: []string{"--build", "-d"}, |
| 100 | + } |
| 101 | + err = p.Up(opts) |
| 102 | + if err != nil { |
| 103 | + return nil, errors.Wrap(err, "could not boot up service using Docker Compose") |
| 104 | + } |
| 105 | + |
| 106 | + // Connect service network with stack network (for the purpose of metrics collection) |
| 107 | + err = docker.ConnectToNetwork(p.ContainerName(serviceName), stack.Network()) |
| 108 | + if err != nil { |
| 109 | + return nil, errors.Wrapf(err, "can't attach service container to the stack network") |
| 110 | + } |
| 111 | + |
| 112 | + err = p.WaitForHealthy(opts) |
| 113 | + if err != nil { |
| 114 | + processServiceContainerLogs(p, compose.CommandOptions{ |
| 115 | + Env: opts.Env, |
| 116 | + }, outCtxt.Name) |
| 117 | + return nil, errors.Wrap(err, "service is unhealthy") |
| 118 | + } |
| 119 | + |
| 120 | + // Build service container name |
| 121 | + outCtxt.Hostname = p.ContainerName(serviceName) |
| 122 | + |
| 123 | + logger.Debugf("adding service container %s internal ports to context", p.ContainerName(serviceName)) |
| 124 | + serviceComposeConfig, err := p.Config(compose.CommandOptions{Env: env}) |
| 125 | + if err != nil { |
| 126 | + return nil, errors.Wrap(err, "could not get Docker Compose configuration for service") |
| 127 | + } |
| 128 | + |
| 129 | + s := serviceComposeConfig.Services[serviceName] |
| 130 | + outCtxt.Ports = make([]int, len(s.Ports)) |
| 131 | + for idx, port := range s.Ports { |
| 132 | + outCtxt.Ports[idx] = port.InternalPort |
| 133 | + } |
| 134 | + |
| 135 | + // Shortcut to first port for convenience |
| 136 | + if len(outCtxt.Ports) > 0 { |
| 137 | + outCtxt.Port = outCtxt.Ports[0] |
| 138 | + } |
| 139 | + |
| 140 | + outCtxt.Agent.Host.NamePrefix = inCtxt.Name |
| 141 | + service.ctxt = outCtxt |
| 142 | + return &service, nil |
| 143 | +} |
| 144 | + |
| 145 | +func (d *CustomAgentDeployer) loadComposeDefinitions() ([]string, error) { |
| 146 | + locationManager, err := locations.NewLocationManager() |
| 147 | + if err != nil { |
| 148 | + return nil, errors.Wrap(err, "can't locate Docker Compose file for Custom Agent deployer") |
| 149 | + } |
| 150 | + return []string{locationManager.DockerCustomAgentDeployerYml(), d.cfg}, nil |
| 151 | +} |
0 commit comments