internal/stack/resources.go (223 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 ( "bytes" "embed" "fmt" "html/template" "os" "path/filepath" "slices" "strings" "github.com/Masterminds/semver/v3" "github.com/elastic/go-resource" "github.com/elastic/elastic-package/internal/profile" ) //go:embed _static var static embed.FS const ( // ComposeFile is the docker compose file. ComposeFile = "docker-compose.yml" // ElasticsearchConfigFile is the elasticsearch config file. ElasticsearchConfigFile = "elasticsearch.yml" // KibanaConfigFile is the kibana config file. KibanaConfigFile = "kibana.yml" // LogstashConfigFile is the logstash config file. LogstashConfigFile = "logstash.conf" // KibanaHealthcheckFile is the kibana healthcheck. KibanaHealthcheckFile = "kibana-healthcheck.sh" // FleetServerHealthcheckFile is the Fleet Server healthcheck. FleetServerHealthcheckFile = "fleet-server-healthcheck.sh" // PackageRegistryConfigFile is the config file for the Elastic Package registry PackageRegistryConfigFile = "package-registry.yml" // ElasticAgentEnvFile is the elastic agent environment variables file. ElasticAgentEnvFile = "elastic-agent.env" ElasticAgentFolder = "elastic-agent" CertsFolder = "certs" ProfileStackPath = "stack" elasticsearchUsername = "elastic" elasticsearchPassword = "changeme" configAPMEnabled = "stack.apm_enabled" configGeoIPDir = "stack.geoip_dir" configKibanaHTTP2Enabled = "stack.kibana_http2_enabled" configLogsDBEnabled = "stack.logsdb_enabled" configLogstashEnabled = "stack.logstash_enabled" configSelfMonitorEnabled = "stack.self_monitor_enabled" configElasticSubscription = "stack.elastic_subscription" ) var ( templateFuncs = template.FuncMap{ "semverLessThan": semverLessThan, "indent": indent, } staticSource = resource.NewSourceFS(static).WithTemplateFuncs(templateFuncs) stackResources = []resource.Resource{ &resource.File{ Path: "Dockerfile.package-registry", Content: staticSource.Template("_static/Dockerfile.package-registry.tmpl"), }, &resource.File{ Path: ComposeFile, Content: staticSource.Template("_static/docker-compose-stack.yml.tmpl"), }, &resource.File{ Path: ElasticsearchConfigFile, Content: staticSource.Template("_static/elasticsearch.yml.tmpl"), }, &resource.File{ Path: "service_tokens", Content: staticSource.File("_static/service_tokens"), }, &resource.File{ Path: "ingest-geoip/GeoLite2-ASN.mmdb", CreateParent: true, Content: staticSource.File("_static/GeoLite2-ASN.mmdb"), }, &resource.File{ Path: "ingest-geoip/GeoLite2-City.mmdb", CreateParent: true, Content: staticSource.File("_static/GeoLite2-City.mmdb"), }, &resource.File{ Path: "ingest-geoip/GeoLite2-Country.mmdb", CreateParent: true, Content: staticSource.File("_static/GeoLite2-Country.mmdb"), }, &resource.File{ Path: KibanaConfigFile, Content: staticSource.Template("_static/kibana.yml.tmpl"), }, &resource.File{ Path: KibanaHealthcheckFile, Content: staticSource.Template("_static/kibana-healthcheck.sh.tmpl"), }, &resource.File{ Path: FleetServerHealthcheckFile, Content: staticSource.File("_static/fleet-server-healthcheck.sh"), }, &resource.File{ Path: PackageRegistryConfigFile, Content: staticSource.File("_static/package-registry.yml"), }, &resource.File{ Path: ElasticAgentEnvFile, Content: staticSource.Template("_static/elastic-agent.env.tmpl"), }, } logstashResources = []resource.Resource{ &resource.File{ Path: LogstashConfigFile, Content: staticSource.Template("_static/logstash.conf.tmpl"), }, &resource.File{ Path: "Dockerfile.logstash", Content: staticSource.File("_static/Dockerfile.logstash"), }, } elasticSubscriptionsSupported = []string{ "basic", "trial", } ) func applyResources(profile *profile.Profile, stackVersion string) error { stackDir := filepath.Join(profile.ProfilePath, ProfileStackPath) var agentPorts []string if err := profile.Decode("stack.agent.ports", &agentPorts); err != nil { return fmt.Errorf("failed to unmarshal stack.agent.ports: %w", err) } elasticSubscriptionProfile := profile.Config(configElasticSubscription, "trial") if !slices.Contains(elasticSubscriptionsSupported, elasticSubscriptionProfile) { return fmt.Errorf("unsupported Elastic subscription %q: supported subscriptions: %s", elasticSubscriptionProfile, strings.Join(elasticSubscriptionsSupported, ", ")) } resourceManager := resource.NewManager() resourceManager.AddFacter(resource.StaticFacter{ "registry_base_image": PackageRegistryBaseImage, "elasticsearch_version": stackVersion, "kibana_version": stackVersion, "agent_version": stackVersion, "kibana_host": "https://kibana:5601", "fleet_url": "https://fleet-server:8220", "elasticsearch_host": "https://elasticsearch:9200", "api_key": "", "username": elasticsearchUsername, "password": elasticsearchPassword, "enrollment_token": "", "agent_publish_ports": strings.Join(agentPorts, ","), "apm_enabled": profile.Config(configAPMEnabled, "false"), "geoip_dir": profile.Config(configGeoIPDir, "./ingest-geoip"), "kibana_http2_enabled": profile.Config(configKibanaHTTP2Enabled, "true"), "logsdb_enabled": profile.Config(configLogsDBEnabled, "false"), "logstash_enabled": profile.Config(configLogstashEnabled, "false"), "self_monitor_enabled": profile.Config(configSelfMonitorEnabled, "false"), "elastic_subscription": elasticSubscriptionProfile, }) if err := os.MkdirAll(stackDir, 0755); err != nil { return fmt.Errorf("failed to create stack directory: %w", err) } resourceManager.RegisterProvider("file", &resource.FileProvider{ Prefix: stackDir, }) resources := append([]resource.Resource{}, stackResources...) // Keeping certificates in the profile directory for backwards compatibility reasons. resourceManager.RegisterProvider(CertsFolder, &resource.FileProvider{ Prefix: profile.ProfilePath, }) certResources, err := initTLSCertificates(CertsFolder, profile.ProfilePath, tlsServices) if err != nil { return fmt.Errorf("failed to create TLS files: %w", err) } resources = append(resources, certResources...) // Add related resources and client certificates if logstash is enabled. if profile.Config("stack.logstash_enabled", "false") == "true" { resources = append(resources, logstashResources...) if err := addClientCertsToResources(resourceManager, certResources); err != nil { return fmt.Errorf("error adding client certificates: %w", err) } } results, err := resourceManager.Apply(resources) if err != nil { var errors []string for _, result := range results { if err := result.Err(); err != nil { errors = append(errors, err.Error()) } } return fmt.Errorf("%w: %s", err, strings.Join(errors, ", ")) } return nil } func addClientCertsToResources(resourceManager *resource.Manager, certResources []resource.Resource) error { certPath := filepath.Join(CertsFolder, ElasticAgentFolder, "cert.pem") keyPath := filepath.Join(CertsFolder, ElasticAgentFolder, "key.pem") var certFile, keyFile string var err error for _, r := range certResources { res, _ := r.(*resource.File) if strings.Contains(res.Path, ElasticAgentFolder) { var buf bytes.Buffer if res.Path == certPath { err = res.Content(nil, &buf) if err != nil { return fmt.Errorf("failed to read client certificate: %w", err) } // Replace newlines with spaces to create proper indentation in the config certFile = buf.String() continue } if res.Path == keyPath { err = res.Content(nil, &buf) if err != nil { return fmt.Errorf("failed to read client key: %w", err) } // Replace newlines with spaces to create proper indentation in the config keyFile = buf.String() continue } } } resourceManager.AddFacter(resource.StaticFacter{ "agent_certificate": certFile, "agent_key": keyFile, }) return nil } func semverLessThan(a, b string) (bool, error) { sa, err := semver.NewVersion(a) if err != nil { return false, fmt.Errorf("%w: %q", err, a) } sb, err := semver.NewVersion(b) if err != nil { return false, fmt.Errorf("%w: %q", err, b) } return sa.LessThan(sb), nil } // indent appends the indent string to the right of input string. // Typically used for fixing yaml configs. func indent(input string, indent string) string { return strings.ReplaceAll(input, "\n", "\n"+indent) }