internal/install/application_configuration.go (202 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 install
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"github.com/elastic/elastic-package/internal/configuration/locations"
"github.com/elastic/elastic-package/internal/environment"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/profile"
)
const (
stackVersion715 = "7.15.0-SNAPSHOT"
stackVersion820 = "8.2.0-SNAPSHOT"
// Not setting here 8.16.0-SNAPSHOT to take also into account prerelease versions
// like 8.16.0-21bba6f5-SNAPSHOT
stackVersion8160 = "8.16.0-00000000-SNAPSHOT"
elasticAgentLegacyImageName = "docker.elastic.co/beats/elastic-agent"
elasticAgentImageName = "docker.elastic.co/elastic-agent/elastic-agent"
elasticAgentCompleteLegacyImageName = "docker.elastic.co/beats/elastic-agent-complete"
elasticAgentCompleteImageName = "docker.elastic.co/elastic-agent/elastic-agent-complete"
elasticAgentCompleteWolfiImageName = "docker.elastic.co/elastic-agent/elastic-agent-complete-wolfi"
elasticAgentWolfiImageName = "docker.elastic.co/elastic-agent/elastic-agent-wolfi"
elasticsearchImageName = "docker.elastic.co/elasticsearch/elasticsearch"
kibanaImageName = "docker.elastic.co/kibana/kibana"
logstashImageName = "docker.elastic.co/logstash/logstash"
applicationConfigurationYmlFile = "config.yml"
)
var (
elasticAgentCompleteFirstSupportedVersion = semver.MustParse(stackVersion715)
elasticAgentCompleteOwnNamespaceVersion = semver.MustParse(stackVersion820)
elasticAgentWolfiVersion = semver.MustParse(stackVersion8160)
// ProfileNameEnvVar is the name of the environment variable to set the default profile
ProfileNameEnvVar = environment.WithElasticPackagePrefix("PROFILE")
disableElasticAgentWolfiEnvVar = environment.WithElasticPackagePrefix("DISABLE_ELASTIC_AGENT_WOLFI")
)
func DefaultConfiguration() *ApplicationConfiguration {
config := ApplicationConfiguration{}
config.c.Profile.Current = profile.DefaultProfile
// Uncomment and use the commented definition of "stack" in case of emergency
// to define Docker image overrides (stack.image_ref_overrides).
// The following sample defines overrides for the Elastic stack ver. 7.13.0-SNAPSHOT.
// It's advised to use latest stable snapshots for the stack snapshot.
//
// config.c.Stack.ImageRefOverrides = map[string]ImageRefs{
// "7.13.0-SNAPSHOT": ImageRefs{
// ElasticAgent: elasticAgentImageName + `@sha256:76c294cf55654bc28dde72ce936032f34ad5f40c345f3df964924778b249e581`,
// Kibana: kibanaImageName + `@sha256:78ae3b1ca09efee242d2c77597dfab18670e984adb96c2407ec03fe07ceca4f6`,
// },
// }
return &config
}
// ApplicationConfiguration represents the configuration of the elastic-package.
type ApplicationConfiguration struct {
c configFile
agentBaseImage string
stackVersion string
}
type configFile struct {
Stack stack `yaml:"stack"`
Profile struct {
Current string `yaml:"current"`
} `yaml:"profile"`
}
type stack struct {
ImageRefOverrides map[string]ImageRefs `yaml:"image_ref_overrides"`
}
func checkImageRefOverride(envVar, fallback string) string {
refOverride := os.Getenv(envVar)
return stringOrDefault(refOverride, fallback)
}
func (s stack) ImageRefOverridesForVersion(version string) ImageRefs {
appConfigImageRefs := s.ImageRefOverrides[version]
return ImageRefs{
ElasticAgent: checkImageRefOverride("ELASTIC_AGENT_IMAGE_REF_OVERRIDE", stringOrDefault(appConfigImageRefs.ElasticAgent, "")),
Elasticsearch: checkImageRefOverride("ELASTICSEARCH_IMAGE_REF_OVERRIDE", stringOrDefault(appConfigImageRefs.Elasticsearch, "")),
Kibana: checkImageRefOverride("KIBANA_IMAGE_REF_OVERRIDE", stringOrDefault(appConfigImageRefs.Kibana, "")),
Logstash: checkImageRefOverride("LOGSTASH_IMAGE_REF_OVERRIDE", stringOrDefault(appConfigImageRefs.Logstash, "")),
}
}
// ImageRefs stores Docker image references used to create the Elastic stack containers.
type ImageRefs struct {
ElasticAgent string `yaml:"elastic-agent"`
Elasticsearch string `yaml:"elasticsearch"`
Kibana string `yaml:"kibana"`
Logstash string `yaml:"logstash"`
}
// AsEnv method returns key=value representation of image refs.
func (ir ImageRefs) AsEnv() []string {
var vars []string
vars = append(vars, "ELASTIC_AGENT_IMAGE_REF="+ir.ElasticAgent)
vars = append(vars, "ELASTICSEARCH_IMAGE_REF="+ir.Elasticsearch)
vars = append(vars, "KIBANA_IMAGE_REF="+ir.Kibana)
vars = append(vars, "LOGSTASH_IMAGE_REF="+ir.Logstash)
return vars
}
// StackImageRefs function selects the appropriate set of Docker image references for the given stack version.
func (ac *ApplicationConfiguration) StackImageRefs() ImageRefs {
refs := ac.c.Stack.ImageRefOverridesForVersion(ac.stackVersion)
refs.ElasticAgent = stringOrDefault(refs.ElasticAgent, fmt.Sprintf("%s:%s", selectElasticAgentImageName(ac.stackVersion, ac.agentBaseImage), ac.stackVersion))
refs.Elasticsearch = stringOrDefault(refs.Elasticsearch, fmt.Sprintf("%s:%s", elasticsearchImageName, ac.stackVersion))
refs.Kibana = stringOrDefault(refs.Kibana, fmt.Sprintf("%s:%s", kibanaImageName, ac.stackVersion))
refs.Logstash = stringOrDefault(refs.Logstash, fmt.Sprintf("%s:%s", logstashImageName, ac.stackVersion))
return refs
}
// CurrentProfile returns the current profile, or the default one if not set.
func (ac *ApplicationConfiguration) CurrentProfile() string {
fromEnv := os.Getenv(ProfileNameEnvVar)
if fromEnv != "" {
return fromEnv
}
current := ac.c.Profile.Current
if current == "" {
return profile.DefaultProfile
}
return current
}
// SetCurrentProfile sets the current profile.
func (ac *ApplicationConfiguration) SetCurrentProfile(name string) {
ac.c.Profile.Current = name
}
// selectElasticAgentImageName function returns the appropriate image name for Elastic-Agent depending on the stack version.
// This is mandatory as "elastic-agent-complete" is available since 7.15.0-SNAPSHOT.
func selectElasticAgentImageName(version, agentBaseImage string) string {
if version == "" { // as version is optional and can be empty
return elasticAgentWolfiImageName
}
v, err := semver.NewVersion(version)
if err != nil {
logger.Errorf("stack version not in semver format (value: %s): %v", version, err)
return elasticAgentWolfiImageName
}
shouldUseWolfiImage := shouldUseWolfiImages(v)
switch agentBaseImage {
case "systemd":
return selectElasticAgentSystemDImageName(v)
case "complete":
if shouldUseWolfiImage {
return elasticAgentCompleteWolfiImageName
}
return selectElasticAgentCompleteImageName(v)
default:
if shouldUseWolfiImage {
return elasticAgentWolfiImageName
}
return selectElasticAgentCompleteImageName(v)
}
}
func shouldUseWolfiImages(version *semver.Version) bool {
disableWolfiImages := false
valueEnv, ok := os.LookupEnv(disableElasticAgentWolfiEnvVar)
if ok && strings.ToLower(valueEnv) != "false" {
disableWolfiImages = true
}
return !disableWolfiImages && !version.LessThan(elasticAgentWolfiVersion)
}
func selectElasticAgentCompleteImageName(version *semver.Version) string {
switch {
case !version.LessThan(elasticAgentCompleteOwnNamespaceVersion):
return elasticAgentCompleteImageName
case !version.LessThan(elasticAgentCompleteFirstSupportedVersion):
return elasticAgentCompleteLegacyImageName
default:
return elasticAgentLegacyImageName
}
}
func selectElasticAgentSystemDImageName(version *semver.Version) string {
if !version.LessThan(elasticAgentCompleteOwnNamespaceVersion) {
return elasticAgentImageName
}
return elasticAgentLegacyImageName
}
type configurationOptions struct {
agentBaseImage string
stackVersion string
}
type ConfigurationOption func(*configurationOptions)
// OptionWithAgentBaseImage sets the agent image type to be used.
func OptionWithAgentBaseImage(agentBaseImage string) ConfigurationOption {
return func(opts *configurationOptions) {
opts.agentBaseImage = agentBaseImage
}
}
// OptionWithStackVersion sets the Elastic Stack version to be used.
func OptionWithStackVersion(stackVersion string) ConfigurationOption {
return func(opts *configurationOptions) {
opts.stackVersion = stackVersion
}
}
// Configuration function returns the elastic-package configuration.
func Configuration(options ...ConfigurationOption) (*ApplicationConfiguration, error) {
configPath, err := locations.NewLocationManager()
if err != nil {
return nil, fmt.Errorf("can't read configuration directory: %w", err)
}
cfg, err := os.ReadFile(filepath.Join(configPath.RootDir(), applicationConfigurationYmlFile))
if errors.Is(err, os.ErrNotExist) {
return DefaultConfiguration(), nil
}
if err != nil {
return nil, fmt.Errorf("can't read configuration file: %w", err)
}
var c configFile
err = yaml.Unmarshal(cfg, &c)
if err != nil {
return nil, fmt.Errorf("can't unmarshal configuration file: %w", err)
}
configOptions := configurationOptions{}
for _, option := range options {
option(&configOptions)
}
configuration := ApplicationConfiguration{
c: c,
agentBaseImage: configOptions.agentBaseImage,
stackVersion: configOptions.stackVersion,
}
return &configuration, nil
}
func stringOrDefault(value string, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}