Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0f98577
Add stack provider for Elastic Cloud
jsoriano Apr 21, 2023
8315709
Fix version
jsoriano Apr 21, 2023
8d7b1d7
Merge remote-tracking branch 'origin/main' into stack-cloud-provider
jsoriano May 22, 2023
ff03886
Upload GeoIP databases
jsoriano May 22, 2023
4cede7e
Replace GeoIP database in cloud deployment
jsoriano May 23, 2023
8298102
Start local agent connected to cloud
jsoriano May 23, 2023
1931b77
Create Agent policy
jsoriano May 24, 2023
0938e7a
Fix path to env file
jsoriano May 24, 2023
3a1643f
Retrieve Fleet Server url from Fleet API
jsoriano May 24, 2023
7f1450a
Merge remote-tracking branch 'origin/main' into stack-cloud-provider
jsoriano May 24, 2023
f71f8cb
Use facts directly in templates where possible
jsoriano May 24, 2023
ee3a31b
Some refactors
jsoriano May 24, 2023
6255827
Reduce size of deployment
jsoriano May 25, 2023
9b22f2c
Parameterize deployment
jsoriano May 25, 2023
4565592
Update deployments on stack up
jsoriano May 25, 2023
4864a3d
Use diferent compose project names per project
jsoriano May 25, 2023
42b95a7
Install zipped package
jsoriano May 25, 2023
a41b053
Merge remote-tracking branch 'origin/main' into stack-cloud-provider
jsoriano May 26, 2023
ed9e89d
Remove TODO
jsoriano May 26, 2023
c61f7db
Merge remote-tracking branch 'origin/main' into stack-cloud-provider
jsoriano Jul 12, 2023
74b5d1e
Merge remote-tracking branch 'origin/main' into stack-cloud-provider
jsoriano Jul 25, 2023
7a3f2f2
Merge remote-tracking branch 'origin/main' into stack-cloud-provider
jsoriano Jul 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Start local agent connected to cloud
  • Loading branch information
jsoriano committed May 23, 2023
commit 8298102050930a56d85e286a3aae7d46638a2a5c
14 changes: 14 additions & 0 deletions internal/stack/_static/cloud-elastic-agent.env.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{{ $version := fact "agent_version" }}
{{ $fleetURL := fact "fleet_url" }}
{{ $kibanaHost := fact "kibana_host" }}
{{ $username := fact "username" }}
{{ $password := fact "password" }}
FLEET_ENROLL=1
FLEET_URL={{ $fleetURL }}
KIBANA_FLEET_HOST={{ $kibanaHost }}
KIBANA_HOST={{ $kibanaHost }}
ELASTICSEARCH_USERNAME={{ $username }}
ELASTICSEARCH_PASSWORD={{ $password }}
{{ if not (semverLessThan $version "8.0.0") }}
FLEET_TOKEN_POLICY_NAME=Elastic-Agent (elastic-package)
{{ end }}
27 changes: 27 additions & 0 deletions internal/stack/_static/cloud-elastic-agent.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{ $image := fact "agent_image" }}
version: '2.3'
services:
elastic-agent:
image: "{{ $image }}"
healthcheck:
test: "elastic-agent status"
timeout: 2s
start_period: 360s
retries: 180
interval: 5s
hostname: docker-fleet-agent
env_file: "./elastic-agent.env"
volumes:
- type: bind
source: ../../../tmp/service_logs/
target: /tmp/service_logs/
# Mount service_logs under /run too as a testing workaround for the journald input (see elastic-package#1235).
- type: bind
source: ../../../tmp/service_logs/
target: /run/service_logs/

elastic-agent_is_ready:
image: tianon/true
depends_on:
elastic-agent:
condition: service_healthy
140 changes: 120 additions & 20 deletions internal/stack/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import (
"github.com/elastic/cloud-sdk-go/pkg/plan"
"github.com/elastic/cloud-sdk-go/pkg/plan/planutil"

"github.com/elastic/elastic-package/internal/compose"
"github.com/elastic/elastic-package/internal/docker"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/profile"
)
Expand Down Expand Up @@ -72,7 +74,8 @@ func (cp *cloudProvider) BootUp(options Options) error {
logger.Warn("Elastic Cloud provider is in technical preview")

_, err := cp.currentDeployment()
if err == nil {
switch err {
case nil:
// Do nothing, deployment already exists.
// TODO: Migrate configuration if changed.
config, err := LoadConfig(cp.profile)
Expand All @@ -81,7 +84,10 @@ func (cp *cloudProvider) BootUp(options Options) error {
}
printUserConfig(options.Printer, config)
return nil
} else if err != nil && err != errDeploymentNotExist {
case errDeploymentNotExist:
// Deployment doesn't exist, let's continue.
break
default:
return err
}

Expand Down Expand Up @@ -221,7 +227,7 @@ func (cp *cloudProvider) BootUp(options Options) error {
return fmt.Errorf("failed to disable geoip automatic downloader: %w", err)
}
if resp.IsError() {
return fmt.Errorf("failed to disable geoup automatic downloader (status: %v): %v:", resp.StatusCode, resp.String())
return fmt.Errorf("failed to disable geoip automatic downloader (status: %v): %v", resp.StatusCode, resp.String())
}

geoIPExtension, err := cp.createGeoIPExtension()
Expand Down Expand Up @@ -286,6 +292,15 @@ func (cp *cloudProvider) BootUp(options Options) error {
return fmt.Errorf("failed to track cluster creation: %w", err)
}

// FIXME: Create initial agent policy.

logger.Debugf("Starting local agent")

err = cp.startLocalAgent(options, config)
if err != nil {
return fmt.Errorf("failed to start local agent: %w", err)
}

return nil
}

Expand Down Expand Up @@ -360,33 +375,41 @@ func zipGeoIPBundle() (*bytes.Buffer, error) {
return &bundle, nil
}

func (cp *cloudProvider) deleteGeoIPExtension() error {
config, err := LoadConfig(cp.profile)
func (cp *cloudProvider) localAgentComposeProject() (*compose.Project, error) {
composeFile := cp.profile.Path(profileStackPath, CloudComposeFile)
return compose.NewProject(DockerComposeProjectName, composeFile)
}

func (cp *cloudProvider) startLocalAgent(options Options, config Config) error {
err := applyCloudResources(cp.profile, options.StackVersion, config)
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
return fmt.Errorf("could not initialize compose files for local agent: %w", err)
}
extensionID, found := config.Parameters[paramGeoIPExtensionID]
if !found {
return nil

project, err := cp.localAgentComposeProject()
if err != nil {
return fmt.Errorf("could not initialize local agent compose project")
}

backoff := retry.NewFibonacci(1 * time.Second)
backoff = retry.WithMaxDuration(180*time.Second, backoff)
retry.Do(context.TODO(), backoff, func(ctx context.Context) error {
err = extensionapi.Delete(extensionapi.DeleteParams{
API: cp.api,
ExtensionID: extensionID,
})
// Actually, we should only retry on extensions.extension_in_use errors.
return retry.RetryableError(err)
})
err = project.Build(compose.CommandOptions{})
if err != nil {
return fmt.Errorf("delete API call failed: %w", err)
return fmt.Errorf("failed to build images for local agent: %w", err)
}

err = project.Up(compose.CommandOptions{})
if err != nil {
return fmt.Errorf("failed to start local agent: %w", err)
}

return nil
}

func (cp *cloudProvider) TearDown(options Options) error {
err := cp.destroyLocalAgent()
if err != nil {
return fmt.Errorf("failed to destroy local agent: %w", err)
}

deployment, err := cp.currentDeployment()
if err != nil {
return fmt.Errorf("failed to find current deployment: %w", err)
Expand Down Expand Up @@ -415,6 +438,46 @@ func (cp *cloudProvider) TearDown(options Options) error {
return nil
}

func (cp *cloudProvider) deleteGeoIPExtension() error {
config, err := LoadConfig(cp.profile)
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}
extensionID, found := config.Parameters[paramGeoIPExtensionID]
if !found {
return nil
}

backoff := retry.NewFibonacci(1 * time.Second)
backoff = retry.WithMaxDuration(180*time.Second, backoff)
retry.Do(context.TODO(), backoff, func(ctx context.Context) error {
err = extensionapi.Delete(extensionapi.DeleteParams{
API: cp.api,
ExtensionID: extensionID,
})
// Actually, we should only retry on extensions.extension_in_use errors.
return retry.RetryableError(err)
})
if err != nil {
return fmt.Errorf("delete API call failed: %w", err)
}
return nil
}

func (cp *cloudProvider) destroyLocalAgent() error {
project, err := cp.localAgentComposeProject()
if err != nil {
return fmt.Errorf("could not initialize local agent compose project")
}

err = project.Down(compose.CommandOptions{})
if err != nil {
return fmt.Errorf("failed to destroy local agent: %w", err)
}

return nil
}

func (*cloudProvider) Update(options Options) error {
fmt.Println("Nothing to do.")
return nil
Expand All @@ -431,6 +494,14 @@ func (cp *cloudProvider) Status(options Options) ([]ServiceStatus, error) {
}

status, _ := cp.deploymentStatus(deployment)

agentStatus, err := cp.localAgentStatus()
if err != nil {
return nil, fmt.Errorf("failed to get local agent status: %w", err)
}

status = append(status, agentStatus...)

return status, nil
}

Expand Down Expand Up @@ -496,6 +567,35 @@ func (*cloudProvider) deploymentStatus(deployment *models.DeploymentGetResponse)
return status, allHealthy
}

func (cp *cloudProvider) localAgentStatus() ([]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)
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
}
logger.Debugf("Adding Service: \"%v\"", service.Name)
services = append(services, *service)
}

return services, nil
}

func (cp *cloudProvider) currentDeployment() (*models.DeploymentGetResponse, error) {
config, err := LoadConfig(cp.profile)
if err != nil {
Expand Down
76 changes: 76 additions & 0 deletions internal/stack/cloudresources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/elastic/go-resource"

"github.com/elastic/elastic-package/internal/install"
"github.com/elastic/elastic-package/internal/profile"
)

const (
// CloudElasticAgentEnvFile is the elastic agent environment variables file for the
// cloud provider.
CloudElasticAgentEnvFile = "cloud-elastic-agent.env"

// CloudComposeFile is the docker-compose snapshot.yml file name.
CloudComposeFile = "cloud-elastic-agent.yml"
)

var (
cloudStackResources = []resource.Resource{
&resource.File{
Path: CloudComposeFile,
Content: staticSource.Template("_static/cloud-elastic-agent.yml.tmpl"),
},
&resource.File{
Path: CloudElasticAgentEnvFile,
Content: staticSource.Template("_static/cloud-elastic-agent.env.tmpl"),
},
}
)

func applyCloudResources(profile *profile.Profile, stackVersion string, config Config) error {
appConfig, err := install.Configuration()
if err != nil {
return fmt.Errorf("can't read application configuration: %w", err)
}

stackDir := filepath.Join(profile.ProfilePath, profileStackPath)

resourceManager := resource.NewManager()
resourceManager.AddFacter(resource.StaticFacter{
"agent_version": stackVersion,
"agent_image": appConfig.StackImageRefs(stackVersion).ElasticAgent,
"username": config.ElasticsearchUsername,
"password": config.ElasticsearchPassword,
"kibana_host": config.KibanaHost,
"fleet_url": config.Parameters["fleet_url"],
})

os.MkdirAll(stackDir, 0755)
resourceManager.RegisterProvider("file", &resource.FileProvider{
Prefix: stackDir,
})

results, err := resourceManager.Apply(cloudStackResources)
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
}