internal/servicedeployer/custom_agent.go (167 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 servicedeployer
import (
"context"
_ "embed"
"fmt"
"path/filepath"
"github.com/elastic/go-resource"
"github.com/elastic/elastic-package/internal/common"
"github.com/elastic/elastic-package/internal/compose"
"github.com/elastic/elastic-package/internal/configuration/locations"
"github.com/elastic/elastic-package/internal/docker"
"github.com/elastic/elastic-package/internal/files"
"github.com/elastic/elastic-package/internal/install"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/profile"
"github.com/elastic/elastic-package/internal/stack"
)
const (
dockerCustomAgentName = "docker-custom-agent"
dockerCustomAgentDir = "docker_custom_agent"
dockerCustomAgentDockerfile = "docker-custom-agent-base.yml"
)
//go:embed _static/docker-custom-agent-base.yml
var dockerCustomAgentDockerfileContent []byte
// CustomAgentDeployer knows how to deploy a custom elastic-agent defined via
// a Docker Compose file.
type CustomAgentDeployer struct {
profile *profile.Profile
dockerComposeFile string
stackVersion string
policyName string
runTearDown bool
runTestsOnly bool
}
type CustomAgentDeployerOptions struct {
Profile *profile.Profile
DockerComposeFile string
StackVersion string
PolicyName string
RunTearDown bool
RunTestsOnly bool
}
var _ ServiceDeployer = new(CustomAgentDeployer)
// NewCustomAgentDeployer returns a new instance of a deployedCustomAgent.
func NewCustomAgentDeployer(options CustomAgentDeployerOptions) (*CustomAgentDeployer, error) {
return &CustomAgentDeployer{
profile: options.Profile,
dockerComposeFile: options.DockerComposeFile,
stackVersion: options.StackVersion,
policyName: options.PolicyName,
runTearDown: options.RunTearDown,
runTestsOnly: options.RunTestsOnly,
}, nil
}
// SetUp sets up the service and returns any relevant information.
func (d *CustomAgentDeployer) SetUp(ctx context.Context, svcInfo ServiceInfo) (DeployedService, error) {
logger.Warn("DEPRECATED - setting up service using Docker Compose service deployer")
appConfig, err := install.Configuration(install.OptionWithStackVersion(d.stackVersion))
if err != nil {
return nil, fmt.Errorf("can't read application configuration: %w", err)
}
caCertPath, err := stack.FindCACertificate(d.profile)
if err != nil {
return nil, fmt.Errorf("can't locate CA certificate: %w", err)
}
// Build service container name
// FIXME: Currently, this service deployer starts a new agent on its own and
// it cannot use directly the `svcInfo.AgentHostname` value
// Set alias for custom agent
svcInfo.Hostname = dockerCustomAgentName
env := append(
appConfig.StackImageRefs().AsEnv(),
fmt.Sprintf("%s=%s", serviceLogsDirEnv, svcInfo.Logs.Folder.Local),
fmt.Sprintf("%s=%s", localCACertEnv, caCertPath),
fmt.Sprintf("%s=%s", fleetPolicyEnv, d.policyName),
)
configDir, err := d.installDockerfile(deployerFolderName(svcInfo))
if err != nil {
return nil, fmt.Errorf("could not create resources for custom agent: %w", err)
}
ymlPaths := []string{
d.dockerComposeFile,
filepath.Join(configDir, dockerCustomAgentDockerfile),
}
service := dockerComposeDeployedService{
ymlPaths: ymlPaths,
project: fmt.Sprintf("elastic-package-service-%s", svcInfo.Test.RunID),
variant: ServiceVariant{
Name: dockerCustomAgentName,
Env: env,
},
configDir: configDir,
}
p, err := compose.NewProject(service.project, service.ymlPaths...)
if err != nil {
return nil, fmt.Errorf("could not create Docker Compose project for service: %w", err)
}
// Verify the Elastic stack network
err = stack.EnsureStackNetworkUp(d.profile)
if err != nil {
return nil, fmt.Errorf("stack network is not ready: %w", err)
}
// Clean service logs
if d.runTestsOnly {
// service logs folder must no be deleted to avoid breaking log files written
// by the service. If this is required, those files should be rotated or truncated
// so the service can still write to them.
logger.Debugf("Skipping removing service logs folder folder %s", svcInfo.Logs.Folder.Local)
} else {
err = files.RemoveContent(svcInfo.Logs.Folder.Local)
if err != nil {
return nil, fmt.Errorf("removing service logs failed: %w", err)
}
}
svcInfo.Name = dockerCustomAgentName
serviceName := svcInfo.Name
opts := compose.CommandOptions{
Env: env,
ExtraArgs: []string{"--build", "-d"},
}
if d.runTestsOnly || d.runTearDown {
logger.Debug("Skipping bringing up docker-compose project and connect container to network (non setup steps)")
} else {
err = p.Up(ctx, opts)
if err != nil {
return nil, fmt.Errorf("could not boot up service using Docker Compose: %w", err)
}
// TODO: if this agent is moved to "agentdeployer", this container should be connected
// to the network of the agent as done in servicedeployer/compose.go
// Connect service network with stack network (for the purpose of metrics collection)
err = docker.ConnectToNetwork(p.ContainerName(serviceName), stack.Network(d.profile))
if err != nil {
return nil, fmt.Errorf("can't attach service container to the stack network: %w", err)
}
}
// requires to be connected the service to the stack network
err = p.WaitForHealthy(ctx, opts)
if err != nil {
processServiceContainerLogs(ctx, p, compose.CommandOptions{
Env: opts.Env,
}, svcInfo.Name)
return nil, fmt.Errorf("service is unhealthy: %w", err)
}
logger.Debugf("adding service container %s internal ports to context", p.ContainerName(serviceName))
serviceComposeConfig, err := p.Config(ctx, compose.CommandOptions{Env: env})
if err != nil {
return nil, fmt.Errorf("could not get Docker Compose configuration for service: %w", err)
}
s := serviceComposeConfig.Services[serviceName]
svcInfo.Ports = make([]int, len(s.Ports))
for idx, port := range s.Ports {
svcInfo.Ports[idx] = port.InternalPort
}
// Shortcut to first port for convenience
if len(svcInfo.Ports) > 0 {
svcInfo.Port = svcInfo.Ports[0]
}
svcInfo.Agent.Independent = true
svcInfo.Agent.Host.NamePrefix = svcInfo.Name
service.svcInfo = svcInfo
return &service, nil
}
// installDockerfile creates the files needed to run the custom elastic agent and returns
// the directory with these files.
func (d *CustomAgentDeployer) installDockerfile(folder string) (string, error) {
locationManager, err := locations.NewLocationManager()
if err != nil {
return "", fmt.Errorf("failed to find the configuration directory: %w", err)
}
customAgentDir := filepath.Join(locationManager.DeployerDir(), dockerCustomAgentDir, folder)
resources := []resource.Resource{
&resource.File{
Provider: "file",
Path: dockerCustomAgentDockerfile,
Content: resource.FileContentLiteral(string(dockerCustomAgentDockerfileContent)),
CreateParent: true,
},
}
resourceManager := resource.NewManager()
resourceManager.RegisterProvider("file", &resource.FileProvider{
Prefix: customAgentDir,
})
results, err := resourceManager.Apply(resources)
if err != nil {
return "", fmt.Errorf("%w: %s", err, common.ProcessResourceApplyResults(results))
}
return customAgentDir, nil
}