diff --git a/cli/cmd/run.go b/cli/cmd/run.go index cbed0499d9..35882d3256 100644 --- a/cli/cmd/run.go +++ b/cli/cmd/run.go @@ -64,7 +64,7 @@ func buildRunServiceCommand(srv string) *cobra.Command { env := config.PutServiceEnvironment(map[string]string{}, srv, versionToRun) - err := serviceManager.RunCompose(false, []string{srv}, env) + err := serviceManager.RunCompose(context.Background(), false, []string{srv}, env) if err != nil { log.WithFields(log.Fields{ "service": srv, @@ -86,7 +86,7 @@ func buildRunProfileCommand(key string, profile config.Profile) *cobra.Command { "profileVersion": versionToRun, } - err := serviceManager.RunCompose(true, []string{key}, env) + err := serviceManager.RunCompose(context.Background(), true, []string{key}, env) if err != nil { log.WithFields(log.Fields{ "profile": key, diff --git a/cli/cmd/stop.go b/cli/cmd/stop.go index d20cab4ece..35f3681cda 100644 --- a/cli/cmd/stop.go +++ b/cli/cmd/stop.go @@ -5,6 +5,8 @@ package cmd import ( + "context" + "github.com/elastic/e2e-testing/cli/config" "github.com/elastic/e2e-testing/cli/services" log "github.com/sirupsen/logrus" @@ -55,7 +57,7 @@ func buildStopServiceCommand(srv string) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { serviceManager := services.NewServiceManager() - err := serviceManager.StopCompose(false, []string{srv}) + err := serviceManager.StopCompose(context.Background(), false, []string{srv}) if err != nil { log.WithFields(log.Fields{ "service": srv, @@ -73,7 +75,7 @@ func buildStopProfileCommand(key string, profile config.Profile) *cobra.Command Run: func(cmd *cobra.Command, args []string) { serviceManager := services.NewServiceManager() - err := serviceManager.StopCompose(true, []string{key}) + err := serviceManager.StopCompose(context.Background(), true, []string{key}) if err != nil { log.WithFields(log.Fields{ "profile": key, diff --git a/cli/config/compose/profiles/helm/docker-compose.yml b/cli/config/compose/profiles/helm/docker-compose.yml new file mode 100644 index 0000000000..dcb02ffd92 --- /dev/null +++ b/cli/config/compose/profiles/helm/docker-compose.yml @@ -0,0 +1,14 @@ +version: '2.4' +services: + elasticsearch: + environment: + - bootstrap.memory_lock=true + - discovery.type=single-node + - ES_JAVA_OPTS=-Xms512m -Xmx512m + - xpack.security.enabled=false + - xpack.monitoring.collection.enabled=true + - ELASTIC_USERNAME=elastic + - ELASTIC_PASSWORD=changeme + image: "docker.elastic.co/elasticsearch/elasticsearch:${stackVersion:-8.0.0-SNAPSHOT}" + ports: + - "9200:9200" diff --git a/cli/docker/docker.go b/cli/docker/docker.go index 147d2ca6b5..49c6805a28 100644 --- a/cli/docker/docker.go +++ b/cli/docker/docker.go @@ -201,6 +201,7 @@ func LoadImage(imagePath string) error { } log.WithFields(log.Fields{ + "image": fileNamePath, "response": imageLoadResponse, }).Debug("Docker image loaded successfully") return nil diff --git a/cli/services/helm.go b/cli/services/helm.go index 51e6e9e473..7edbf621c0 100644 --- a/cli/services/helm.go +++ b/cli/services/helm.go @@ -5,18 +5,20 @@ package services import ( + "context" "errors" "strings" "github.com/elastic/e2e-testing/cli/shell" log "github.com/sirupsen/logrus" + "go.elastic.co/apm" ) // HelmManager defines the operations for Helm type HelmManager interface { - AddRepo(repo string, URL string) error - DeleteChart(chart string) error - InstallChart(name string, chart string, version string, flags []string) error + AddRepo(ctx context.Context, repo string, URL string) error + DeleteChart(ctx context.Context, chart string) error + InstallChart(ctx context.Context, name string, chart string, version string, flags []string) error } // HelmFactory returns oone of the Helm supported versions, or an error @@ -39,13 +41,18 @@ type helm3X struct { helm } -func (h *helm3X) AddRepo(repo string, url string) error { +func (h *helm3X) AddRepo(ctx context.Context, repo string, url string) error { + span, _ := apm.StartSpanOptions(ctx, "Adding Helm repository", "helm.repo.add", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + // use chart as application name args := []string{ "repo", "add", repo, url, } - output, err := helmExecute(args...) + output, err := helmExecute(ctx, args...) if err != nil { return err } @@ -58,12 +65,17 @@ func (h *helm3X) AddRepo(repo string, url string) error { return nil } -func (h *helm3X) DeleteChart(chart string) error { +func (h *helm3X) DeleteChart(ctx context.Context, chart string) error { + span, _ := apm.StartSpanOptions(ctx, "Deleting Helm chart", "helm.chart.delete", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + args := []string{ "delete", chart, } - output, err := helmExecute(args...) + output, err := helmExecute(ctx, args...) if err != nil { return err } @@ -75,13 +87,18 @@ func (h *helm3X) DeleteChart(chart string) error { return nil } -func (h *helm3X) InstallChart(name string, chart string, version string, flags []string) error { +func (h *helm3X) InstallChart(ctx context.Context, name string, chart string, version string, flags []string) error { + span, _ := apm.StartSpanOptions(ctx, "Installing Helm chart", "helm.chart.install", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + args := []string{ "install", name, chart, "--version", version, } args = append(args, flags...) - output, err := helmExecute(args...) + output, err := helmExecute(ctx, args...) if err != nil { return err } @@ -95,8 +112,8 @@ func (h *helm3X) InstallChart(name string, chart string, version string, flags [ return nil } -func helmExecute(args ...string) (string, error) { - output, err := shell.Execute(".", "helm", args...) +func helmExecute(ctx context.Context, args ...string) (string, error) { + output, err := shell.Execute(ctx, ".", "helm", args...) if err != nil { return "", err } diff --git a/cli/services/kibana.go b/cli/services/kibana.go index 97271a0168..356351e2ef 100644 --- a/cli/services/kibana.go +++ b/cli/services/kibana.go @@ -5,6 +5,7 @@ package services import ( + "context" "fmt" "strings" "time" @@ -12,6 +13,7 @@ import ( backoff "github.com/cenkalti/backoff/v4" curl "github.com/elastic/e2e-testing/cli/shell" log "github.com/sirupsen/logrus" + "go.elastic.co/apm" ) // KibanaBaseURL All URLs running on localhost as Kibana is expected to be exposed there @@ -238,7 +240,7 @@ func (k *KibanaClient) UpdateIntegrationPackageConfig(packageConfigID string, pa // WaitForKibana waits for kibana running in localhost:5601 to be healthy, returning false // if kibana does not get healthy status in a defined number of minutes. -func (k *KibanaClient) WaitForKibana(maxTimeoutMinutes time.Duration) (bool, error) { +func (k *KibanaClient) WaitForKibana(ctx context.Context, maxTimeoutMinutes time.Duration) (bool, error) { k.withURL("/status") var ( @@ -259,6 +261,11 @@ func (k *KibanaClient) WaitForKibana(maxTimeoutMinutes time.Duration) (bool, err retryCount := 1 kibanaStatus := func() error { + span, _ := apm.StartSpanOptions(ctx, "Health", "kibana.health", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + r := curl.HTTPRequest{ BasicAuthUser: "elastic", BasicAuthPassword: "changeme", diff --git a/cli/services/kubectl.go b/cli/services/kubectl.go index 760e330b48..04c1b576cb 100644 --- a/cli/services/kubectl.go +++ b/cli/services/kubectl.go @@ -5,10 +5,12 @@ package services import ( + "context" "encoding/json" "strings" "github.com/elastic/e2e-testing/cli/shell" + "go.elastic.co/apm" ) // Resource hide the real type of the enum @@ -39,8 +41,13 @@ type Kubectl struct { } // GetStringResourcesBySelector Use kubectl to get a resource identified by a selector, return the resource in a string. -func (k *Kubectl) GetStringResourcesBySelector(resourceType, selector string) (string, error) { - output, err := k.Run("get", resourceType, "--selector", selector, "-o", "json") +func (k *Kubectl) GetStringResourcesBySelector(ctx context.Context, resourceType, selector string) (string, error) { + span, _ := apm.StartSpanOptions(ctx, "Getting String resource by selector", "kubectl.stringResources.getBySelector", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + + output, err := k.Run(ctx, "get", resourceType, "--selector", selector, "-o", "json") if err != nil { return "", err } @@ -49,8 +56,13 @@ func (k *Kubectl) GetStringResourcesBySelector(resourceType, selector string) (s } // GetResourcesBySelector Use kubectl to get a resource identified by a selector, return the resource in a map[string]interface{}. -func (k *Kubectl) GetResourcesBySelector(resourceType, selector string) (map[string]interface{}, error) { - output, err := k.Run("get", resourceType, "--selector", selector, "-o", "json") +func (k *Kubectl) GetResourcesBySelector(ctx context.Context, resourceType, selector string) (map[string]interface{}, error) { + span, _ := apm.StartSpanOptions(ctx, "Getting resource by selector", "kubectl.resources.getBySelector", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + + output, err := k.Run(ctx, "get", resourceType, "--selector", selector, "-o", "json") if err != nil { return nil, err } @@ -59,8 +71,13 @@ func (k *Kubectl) GetResourcesBySelector(resourceType, selector string) (map[str } // GetResourceJSONPath use kubectl to get a resource by type and name, and a jsonpath, and return the definition of the resource in JSON. -func (k *Kubectl) GetResourceJSONPath(resourceType, resource, jsonPath string) (string, error) { - output, err := k.Run("get", resourceType, resource, "-o", "jsonpath='"+jsonPath+"'") +func (k *Kubectl) GetResourceJSONPath(ctx context.Context, resourceType, resource, jsonPath string) (string, error) { + span, _ := apm.StartSpanOptions(ctx, "Getting resource by JSON path", "kubectl.resources.getByJSONPath", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + + output, err := k.Run(ctx, "get", resourceType, resource, "-o", "jsonpath='"+jsonPath+"'") if err != nil { return "{}", err } @@ -68,13 +85,18 @@ func (k *Kubectl) GetResourceJSONPath(resourceType, resource, jsonPath string) ( } // GetResourceSelector use kubectl to get the selector for a resource identified by type and name. -func (k *Kubectl) GetResourceSelector(resourceType, resource string) (string, error) { - output, err := k.GetResourceJSONPath(resourceType, resource, "{.metadata.selfLink}") +func (k *Kubectl) GetResourceSelector(ctx context.Context, resourceType, resource string) (string, error) { + span, _ := apm.StartSpanOptions(ctx, "Getting resource selector", "kubectl.resources.getSelector", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + + output, err := k.GetResourceJSONPath(ctx, resourceType, resource, "{.metadata.selfLink}") if err != nil { return "", err } - output, err = k.Run("get", "--raw", strings.ReplaceAll(output, "'", "")+"/scale") + output, err = k.Run(ctx, "get", "--raw", strings.ReplaceAll(output, "'", "")+"/scale") if err != nil { return "", err } @@ -89,8 +111,8 @@ func (k *Kubectl) GetResourceSelector(resourceType, resource string) (string, er } // Run a kubectl command and return the output -func (k *Kubectl) Run(args ...string) (string, error) { - return shell.Execute(".", "kubectl", args...) +func (k *Kubectl) Run(ctx context.Context, args ...string) (string, error) { + return shell.Execute(ctx, ".", "kubectl", args...) } // jsonToObj Converts a JSON string to a map[string]interface{}. diff --git a/cli/services/manager.go b/cli/services/manager.go index b5e6703028..b67bcbe935 100644 --- a/cli/services/manager.go +++ b/cli/services/manager.go @@ -22,8 +22,8 @@ type ServiceManager interface { AddServicesToCompose(ctx context.Context, profile string, composeNames []string, env map[string]string) error RemoveServicesFromCompose(ctx context.Context, profile string, composeNames []string, env map[string]string) error RunCommand(profile string, composeNames []string, composeArgs []string, env map[string]string) error - RunCompose(isProfile bool, composeNames []string, env map[string]string) error - StopCompose(isProfile bool, composeNames []string) error + RunCompose(ctx context.Context, isProfile bool, composeNames []string, env map[string]string) error + StopCompose(ctx context.Context, isProfile bool, composeNames []string) error } // DockerServiceManager implementation of the service manager interface @@ -37,7 +37,7 @@ func NewServiceManager() ServiceManager { // AddServicesToCompose adds services to a running docker compose func (sm *DockerServiceManager) AddServicesToCompose(ctx context.Context, profile string, composeNames []string, env map[string]string) error { - span, _ := apm.StartSpanOptions(ctx, "Add services to Docker Compose", "docker-compose.service.add", apm.SpanOptions{ + span, _ := apm.StartSpanOptions(ctx, "Add services to Docker Compose", "docker-compose.services.add", apm.SpanOptions{ Parent: apm.SpanFromContext(ctx).TraceContext(), }) defer span.End() @@ -65,7 +65,7 @@ func (sm *DockerServiceManager) AddServicesToCompose(ctx context.Context, profil // RemoveServicesFromCompose removes services from a running docker compose func (sm *DockerServiceManager) RemoveServicesFromCompose(ctx context.Context, profile string, composeNames []string, env map[string]string) error { - span, _ := apm.StartSpanOptions(ctx, "Remove services from Docker Compose", "docker-compose.service.remove", apm.SpanOptions{ + span, _ := apm.StartSpanOptions(ctx, "Remove services from Docker Compose", "docker-compose.services.remove", apm.SpanOptions{ Parent: apm.SpanFromContext(ctx).TraceContext(), }) defer span.End() @@ -111,12 +111,22 @@ func (sm *DockerServiceManager) RunCommand(profile string, composeNames []string } // RunCompose runs a docker compose by its name -func (sm *DockerServiceManager) RunCompose(isProfile bool, composeNames []string, env map[string]string) error { +func (sm *DockerServiceManager) RunCompose(ctx context.Context, isProfile bool, composeNames []string, env map[string]string) error { + span, _ := apm.StartSpanOptions(ctx, "Starting Docker Compose files", "docker-compose.services.up", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + return executeCompose(sm, isProfile, composeNames, []string{"up", "-d"}, env) } // StopCompose stops a docker compose by its name -func (sm *DockerServiceManager) StopCompose(isProfile bool, composeNames []string) error { +func (sm *DockerServiceManager) StopCompose(ctx context.Context, isProfile bool, composeNames []string) error { + span, _ := apm.StartSpanOptions(ctx, "Stopping Docker Compose files", "docker-compose.services.down", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + composeFilePaths := make([]string, len(composeNames)) for i, composeName := range composeNames { b := isProfile diff --git a/cli/shell/shell.go b/cli/shell/shell.go index 28630d9430..c1e3114092 100644 --- a/cli/shell/shell.go +++ b/cli/shell/shell.go @@ -6,12 +6,14 @@ package shell import ( "bytes" + "context" "os" "os/exec" "strconv" "strings" log "github.com/sirupsen/logrus" + "go.elastic.co/apm" ) // CheckInstalledSoftware checks that the required software is present @@ -30,7 +32,12 @@ func CheckInstalledSoftware(binaries []string) { // - workspace: represents the location where to execute the command // - command: represents the name of the binary to execute // - args: represents the arguments to be passed to the command -func Execute(workspace string, command string, args ...string) (string, error) { +func Execute(ctx context.Context, workspace string, command string, args ...string) (string, error) { + span, _ := apm.StartSpanOptions(ctx, "Executing shell command", "shell.command.execute", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + log.WithFields(log.Fields{ "command": command, "args": args, diff --git a/e2e/_suites/fleet/ingest-manager_test.go b/e2e/_suites/fleet/ingest-manager_test.go index 270cd7aea3..33cc8f25a3 100644 --- a/e2e/_suites/fleet/ingest-manager_test.go +++ b/e2e/_suites/fleet/ingest-manager_test.go @@ -147,7 +147,7 @@ func InitializeIngestManagerTestSuite(ctx *godog.TestSuiteContext) { } profile := FleetProfileName - err := serviceManager.RunCompose(true, []string{profile}, profileEnv) + err := serviceManager.RunCompose(context.Background(), true, []string{profile}, profileEnv) if err != nil { log.WithFields(log.Fields{ "profile": profile, @@ -155,7 +155,7 @@ func InitializeIngestManagerTestSuite(ctx *godog.TestSuiteContext) { } minutesToBeHealthy := time.Duration(timeoutFactor) * time.Minute - healthy, err := e2e.WaitForElasticsearch(minutesToBeHealthy) + healthy, err := e2e.WaitForElasticsearch(context.Background(), minutesToBeHealthy) if !healthy { log.WithFields(log.Fields{ "error": err, @@ -163,7 +163,7 @@ func InitializeIngestManagerTestSuite(ctx *godog.TestSuiteContext) { }).Fatal("The Elasticsearch cluster could not get the healthy status") } - healthyKibana, err := kibanaClient.WaitForKibana(minutesToBeHealthy) + healthyKibana, err := kibanaClient.WaitForKibana(context.Background(), minutesToBeHealthy) if !healthyKibana { log.WithFields(log.Fields{ "error": err, @@ -181,7 +181,7 @@ func InitializeIngestManagerTestSuite(ctx *godog.TestSuiteContext) { log.Debug("Destroying Fleet runtime dependencies") profile := FleetProfileName - err := serviceManager.StopCompose(true, []string{profile}) + err := serviceManager.StopCompose(context.Background(), true, []string{profile}) if err != nil { log.WithFields(log.Fields{ "error": err, diff --git a/e2e/_suites/helm/helm_charts_test.go b/e2e/_suites/helm/helm_charts_test.go index 5cf5ec7fe6..83628c2038 100644 --- a/e2e/_suites/helm/helm_charts_test.go +++ b/e2e/_suites/helm/helm_charts_test.go @@ -5,6 +5,7 @@ package main import ( + "context" "fmt" "strings" "time" @@ -12,9 +13,12 @@ import ( "github.com/Jeffail/gabs/v2" "github.com/cenkalti/backoff/v4" "github.com/elastic/e2e-testing/cli/config" + "github.com/elastic/e2e-testing/cli/services" k8s "github.com/elastic/e2e-testing/cli/services" shell "github.com/elastic/e2e-testing/cli/shell" "github.com/elastic/e2e-testing/e2e" + "github.com/elastic/e2e-testing/e2e/steps" + "go.elastic.co/apm" "github.com/cucumber/godog" messages "github.com/cucumber/messages-go/v10" @@ -27,6 +31,8 @@ import ( // It can be overriden by the DEVELOPER_MODE env var var developerMode = false +var elasticAPMActive = false + var helm k8s.HelmManager // timeoutFactor a multiplier for the max timeout when doing backoff retries. @@ -45,9 +51,16 @@ var helmChartVersion = "7.10.0" // kubernetesVersion represents the default version used for Kubernetes var kubernetesVersion = "1.18.2" +// stackVersion is the version of the stack to use +// It can be overriden by STACK_VERSION env var +var stackVersion = "8.0.0-SNAPSHOT" + var testSuite HelmChartTestSuite -func init() { +var tx *apm.Transaction +var stepSpan *apm.Span + +func setupSuite() { config.Init() developerMode = shell.GetEnvBool("DEVELOPER_MODE") @@ -55,11 +68,21 @@ func init() { log.Info("Running in Developer mode 💻: runtime dependencies between different test runs will be reused to speed up dev cycle") } + elasticAPMActive = shell.GetEnvBool("ELASTIC_APM_ACTIVE") + if elasticAPMActive { + log.WithFields(log.Fields{ + "apm-environment": shell.GetEnv("ELASTIC_APM_ENVIRONMENT", "local"), + }).Info("Current execution will be instrumented 🛠") + } + helmVersion = shell.GetEnv("HELM_VERSION", helmVersion) helmChartVersion = shell.GetEnv("HELM_CHART_VERSION", helmChartVersion) kubernetesVersion = shell.GetEnv("HELM_KUBERNETES_VERSION", kubernetesVersion) timeoutFactor = shell.GetEnvInteger("TIMEOUT_FACTOR", timeoutFactor) + stackVersion = shell.GetEnv("STACK_VERSION", stackVersion) + stackVersion = e2e.GetElasticArtifactVersion(stackVersion) + h, err := k8s.HelmFactory(helmVersion) if err != nil { log.Fatalf("Helm could not be initialised: %v", err) @@ -80,12 +103,14 @@ type HelmChartTestSuite struct { KubernetesVersion string // the Kubernetes version for the test Name string // the name of the chart Version string // the helm chart version for the test + // instrumentation + currentContext context.Context } func (ts *HelmChartTestSuite) aClusterIsRunning() error { args := []string{"get", "clusters"} - output, err := shell.Execute(".", "kind", args...) + output, err := shell.Execute(context.Background(), ".", "kind", args...) if err != nil { log.WithField("error", err).Error("Could not check the status of the cluster.") } @@ -99,8 +124,8 @@ func (ts *HelmChartTestSuite) aClusterIsRunning() error { return nil } -func (ts *HelmChartTestSuite) addElasticRepo() error { - err := helm.AddRepo("elastic", "https://helm.elastic.co") +func (ts *HelmChartTestSuite) addElasticRepo(ctx context.Context) error { + err := helm.AddRepo(ctx, "elastic", "https://helm.elastic.co") if err != nil { log.WithField("error", err).Error("Could not add Elastic Helm repo") } @@ -111,7 +136,7 @@ func (ts *HelmChartTestSuite) aResourceContainsTheKey(resource string, key strin lowerResource := strings.ToLower(resource) escapedKey := strings.ReplaceAll(key, ".", `\.`) - output, err := kubectl.Run("get", lowerResource, ts.getResourceName(resource), "-o", `jsonpath="{.data['`+escapedKey+`']}"`) + output, err := kubectl.Run(ts.currentContext, "get", lowerResource, ts.getResourceName(resource), "-o", `jsonpath="{.data['`+escapedKey+`']}"`) if err != nil { return err } @@ -130,7 +155,7 @@ func (ts *HelmChartTestSuite) aResourceContainsTheKey(resource string, key strin func (ts *HelmChartTestSuite) aResourceManagesRBAC(resource string) error { lowerResource := strings.ToLower(resource) - output, err := kubectl.Run("get", lowerResource, ts.getResourceName(resource), "-o", `jsonpath="'{.metadata.labels.chart}'"`) + output, err := kubectl.Run(ts.currentContext, "get", lowerResource, ts.getResourceName(resource), "-o", `jsonpath="'{.metadata.labels.chart}'"`) if err != nil { return err } @@ -147,7 +172,7 @@ func (ts *HelmChartTestSuite) aResourceManagesRBAC(resource string) error { } func (ts *HelmChartTestSuite) aResourceWillExposePods(resourceType string) error { - selector, err := kubectl.GetResourceSelector("deployment", ts.Name+"-"+ts.Name) + selector, err := kubectl.GetResourceSelector(ts.currentContext, "deployment", ts.Name+"-"+ts.Name) if err != nil { return err } @@ -158,7 +183,7 @@ func (ts *HelmChartTestSuite) aResourceWillExposePods(resourceType string) error retryCount := 1 checkEndpointsFn := func() error { - output, err := kubectl.GetStringResourcesBySelector("endpoints", selector) + output, err := kubectl.GetStringResourcesBySelector(ts.currentContext, "endpoints", selector) if err != nil { log.WithFields(log.Fields{ "elapsedTime": exp.GetElapsedTime(), @@ -222,7 +247,7 @@ func (ts *HelmChartTestSuite) aResourceWillExposePods(resourceType string) error } func (ts *HelmChartTestSuite) aResourceWillManagePods(resourceType string) error { - selector, err := kubectl.GetResourceSelector("deployment", ts.Name+"-"+ts.Name) + selector, err := kubectl.GetResourceSelector(ts.currentContext, "deployment", ts.Name+"-"+ts.Name) if err != nil { return err } @@ -241,7 +266,7 @@ func (ts *HelmChartTestSuite) aResourceWillManagePods(resourceType string) error } func (ts *HelmChartTestSuite) checkResources(resourceType, selector string, min int) ([]interface{}, error) { - resources, err := kubectl.GetResourcesBySelector(resourceType, selector) + resources, err := kubectl.GetResourcesBySelector(ts.currentContext, resourceType, selector) if err != nil { return nil, err } @@ -259,11 +284,16 @@ func (ts *HelmChartTestSuite) checkResources(resourceType, selector string, min return items, nil } -func (ts *HelmChartTestSuite) createCluster(k8sVersion string) error { +func (ts *HelmChartTestSuite) createCluster(ctx context.Context, k8sVersion string) error { + span, _ := apm.StartSpanOptions(ctx, "Creating Kind cluster", "kind.cluster.create", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + args := []string{"create", "cluster", "--name", ts.ClusterName, "--image", "kindest/node:v" + k8sVersion} log.Trace("Creating cluster with kind") - output, err := shell.Execute(".", "kind", args...) + output, err := shell.Execute(ctx, ".", "kind", args...) if err != nil { log.WithField("error", err).Error("Could not create the cluster") return err @@ -278,7 +308,7 @@ func (ts *HelmChartTestSuite) createCluster(k8sVersion string) error { } func (ts *HelmChartTestSuite) deleteChart() { - err := helm.DeleteChart(ts.Name) + err := helm.DeleteChart(ts.currentContext, ts.Name) if err != nil { log.WithFields(log.Fields{ "chart": ts.Name, @@ -286,11 +316,11 @@ func (ts *HelmChartTestSuite) deleteChart() { } } -func (ts *HelmChartTestSuite) destroyCluster() error { +func (ts *HelmChartTestSuite) destroyCluster(ctx context.Context) error { args := []string{"delete", "cluster", "--name", ts.ClusterName} log.Trace("Deleting cluster") - output, err := shell.Execute(".", "kind", args...) + output, err := shell.Execute(ctx, ".", "kind", args...) if err != nil { log.WithField("error", err).Error("Could not destroy the cluster") return err @@ -303,7 +333,7 @@ func (ts *HelmChartTestSuite) destroyCluster() error { } func (ts *HelmChartTestSuite) elasticsHelmChartIsInstalled(chart string) error { - return ts.install(chart) + return ts.install(ts.currentContext, chart) } // getFullName returns the name plus version, in lowercase, enclosed in quotes @@ -350,15 +380,20 @@ func (ts *HelmChartTestSuite) getResourceName(resource string) string { return "" } -func (ts *HelmChartTestSuite) install(chart string) error { +func (ts *HelmChartTestSuite) install(ctx context.Context, chart string) error { ts.Name = chart elasticChart := "elastic/" + ts.Name flags := []string{} if chart == "elasticsearch" { + span, _ := apm.StartSpanOptions(ctx, "Adding Rancher Local Path Provisioner", "rancher.localpathprovisioner.add", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + // Rancher Local Path Provisioner and local-path storage class for Elasticsearch volumes - _, err := kubectl.Run("apply", "-f", "https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml") + _, err := kubectl.Run(ctx, "apply", "-f", "https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml") if err != nil { log.Errorf("Could not apply Rancher Local Path Provisioner: %v", err) return err @@ -373,13 +408,13 @@ func (ts *HelmChartTestSuite) install(chart string) error { flags = []string{"--wait", fmt.Sprintf("--timeout=%ds", maxTimeout), "--values", "https://raw.githubusercontent.com/elastic/helm-charts/master/elasticsearch/examples/kubernetes-kind/values.yaml"} } - return helm.InstallChart(ts.Name, elasticChart, ts.Version, flags) + return helm.InstallChart(ctx, ts.Name, elasticChart, ts.Version, flags) } -func (ts *HelmChartTestSuite) installRuntimeDependencies(dependencies ...string) error { +func (ts *HelmChartTestSuite) installRuntimeDependencies(ctx context.Context, dependencies ...string) error { for _, dependency := range dependencies { // Install Elasticsearch - err := ts.install(dependency) + err := ts.install(ctx, dependency) if err != nil { log.WithFields(log.Fields{ "dependency": dependency, @@ -393,7 +428,7 @@ func (ts *HelmChartTestSuite) installRuntimeDependencies(dependencies ...string) } func (ts *HelmChartTestSuite) podsManagedByDaemonSet() error { - output, err := kubectl.Run("get", "daemonset", "--namespace=default", "-l", "app="+ts.Name+"-"+ts.Name, "-o", "jsonpath='{.items[0].metadata.labels.chart}'") + output, err := kubectl.Run(ts.currentContext, "get", "daemonset", "--namespace=default", "-l", "app="+ts.Name+"-"+ts.Name, "-o", "jsonpath='{.items[0].metadata.labels.chart}'") if err != nil { return err } @@ -410,7 +445,7 @@ func (ts *HelmChartTestSuite) podsManagedByDaemonSet() error { } func (ts *HelmChartTestSuite) resourceConstraintsAreApplied(constraint string) error { - output, err := kubectl.Run("get", "pods", "-l", "app="+ts.getPodName(), "-o", "jsonpath='{.items[0].spec.containers[0].resources."+constraint+"}'") + output, err := kubectl.Run(ts.currentContext, "get", "pods", "-l", "app="+ts.getPodName(), "-o", "jsonpath='{.items[0].spec.containers[0].resources."+constraint+"}'") if err != nil { return err } @@ -430,7 +465,7 @@ func (ts *HelmChartTestSuite) resourceConstraintsAreApplied(constraint string) e func (ts *HelmChartTestSuite) resourceWillManageAdditionalPodsForMetricsets(resource string) error { lowerResource := strings.ToLower(resource) - output, err := kubectl.Run("get", lowerResource, ts.getResourceName(resource), "-o", "jsonpath='{.metadata.labels.chart}'") + output, err := kubectl.Run(ts.currentContext, "get", lowerResource, ts.getResourceName(resource), "-o", "jsonpath='{.metadata.labels.chart}'") if err != nil { return err } @@ -459,7 +494,7 @@ func (ts *HelmChartTestSuite) strategyCanBeUsedForResourceDuringUpdates(strategy strategyKey = "updateStrategy" } - output, err := kubectl.Run("get", lowerResource, name, "-o", `go-template={{.spec.`+strategyKey+`.type}}`) + output, err := kubectl.Run(ts.currentContext, "get", lowerResource, name, "-o", `go-template={{.spec.`+strategyKey+`.type}}`) if err != nil { return err } @@ -484,7 +519,7 @@ func (ts *HelmChartTestSuite) volumeMountedWithSubpath(name string, mountPath st getMountValues := func(key string) ([]string, error) { // build the arguments for capturing the volume mounts - output, err := kubectl.Run("get", "pods", "-l", "app="+ts.getPodName(), "-o", `jsonpath="{.items[0].spec.containers[0].volumeMounts[*]['`+key+`']}"`) + output, err := kubectl.Run(ts.currentContext, "get", "pods", "-l", "app="+ts.getPodName(), "-o", `jsonpath="{.items[0].spec.containers[0].volumeMounts[*]['`+key+`']}"`) if err != nil { return []string{}, err } @@ -549,7 +584,7 @@ func (ts *HelmChartTestSuite) volumeMountedWithSubpath(name string, mountPath st func (ts *HelmChartTestSuite) willRetrieveSpecificMetrics(chartName string) error { kubeStateMetrics := "kube-state-metrics" - output, err := kubectl.Run("get", "deployment", ts.Name+"-"+kubeStateMetrics, "-o", "jsonpath='{.metadata.name}'") + output, err := kubectl.Run(ts.currentContext, "get", "deployment", ts.Name+"-"+kubeStateMetrics, "-o", "jsonpath='{.metadata.name}'") if err != nil { return err } @@ -566,15 +601,35 @@ func (ts *HelmChartTestSuite) willRetrieveSpecificMetrics(chartName string) erro } func InitializeHelmChartScenario(ctx *godog.ScenarioContext) { - ctx.BeforeScenario(func(*messages.Pickle) { + ctx.BeforeScenario(func(p *messages.Pickle) { log.Trace("Before Helm scenario...") + + tx = apm.DefaultTracer.StartTransaction(p.GetName(), "test.scenario") + tx.Context.SetLabel("suite", "helm") }) ctx.AfterScenario(func(*messages.Pickle, error) { + f := func() { + tx.End() + + apm.DefaultTracer.Flush(nil) + } + defer f() + log.Trace("After Helm scenario...") testSuite.deleteChart() }) + ctx.BeforeStep(func(step *godog.Step) { + stepSpan = tx.StartSpan(step.GetText(), "test.scenario.step", nil) + testSuite.currentContext = apm.ContextWithSpan(context.Background(), stepSpan) + }) + ctx.AfterStep(func(st *godog.Step, err error) { + if stepSpan != nil { + stepSpan.End() + } + }) + ctx.Step(`^a cluster is running$`, testSuite.aClusterIsRunning) ctx.Step(`^the "([^"]*)" Elastic\'s helm chart is installed$`, testSuite.elasticsHelmChartIsInstalled) ctx.Step(`^a pod will be deployed on each node of the cluster by a DaemonSet$`, testSuite.podsManagedByDaemonSet) @@ -594,30 +649,85 @@ func InitializeHelmChartScenario(ctx *godog.ScenarioContext) { func InitializeHelmChartTestSuite(ctx *godog.TestSuiteContext) { ctx.BeforeSuite(func() { + setupSuite() log.Trace("Before Suite...") toolsAreInstalled() - err := testSuite.createCluster(testSuite.KubernetesVersion) + var suiteTx *apm.Transaction + var suiteParentSpan *apm.Span + var suiteContext = context.Background() + + // instrumentation + defer apm.DefaultTracer.Flush(nil) + suiteTx = apm.DefaultTracer.StartTransaction("Initialise Helm", "test.suite") + defer suiteTx.End() + suiteParentSpan = suiteTx.StartSpan("Before Helm test suite", "test.suite.before", nil) + suiteContext = apm.ContextWithSpan(suiteContext, suiteParentSpan) + defer suiteParentSpan.End() + + if elasticAPMActive { + serviceManager := services.NewServiceManager() + + env := map[string]string{ + "stackVersion": stackVersion, + } + + err := serviceManager.RunCompose(suiteContext, true, []string{"helm"}, env) + if err != nil { + log.WithFields(log.Fields{ + "profile": "metricbeat", + }).Warn("Could not run the profile.") + } + steps.AddAPMServicesForInstrumentation(suiteContext, "helm", stackVersion, true, env) + } + + err := testSuite.createCluster(suiteContext, testSuite.KubernetesVersion) if err != nil { return } - err = testSuite.addElasticRepo() + err = testSuite.addElasticRepo(suiteContext) if err != nil { return } - err = testSuite.installRuntimeDependencies("elasticsearch") + err = testSuite.installRuntimeDependencies(suiteContext, "elasticsearch") if err != nil { return } }) ctx.AfterSuite(func() { + f := func() { + apm.DefaultTracer.Flush(nil) + } + defer f() + + // instrumentation + var suiteTx *apm.Transaction + var suiteParentSpan *apm.Span + var suiteContext = context.Background() + defer apm.DefaultTracer.Flush(nil) + suiteTx = apm.DefaultTracer.StartTransaction("Tear Down Helm", "test.suite") + defer suiteTx.End() + suiteParentSpan = suiteTx.StartSpan("After Helm test suite", "test.suite.after", nil) + suiteContext = apm.ContextWithSpan(suiteContext, suiteParentSpan) + defer suiteParentSpan.End() + if !developerMode { log.Trace("After Suite...") - err := testSuite.destroyCluster() + err := testSuite.destroyCluster(suiteContext) if err != nil { return } + + if elasticAPMActive { + serviceManager := services.NewServiceManager() + err := serviceManager.StopCompose(suiteContext, true, []string{"helm"}) + if err != nil { + log.WithFields(log.Fields{ + "profile": "helm", + }).Error("Could not stop the profile.") + } + } } }) diff --git a/e2e/_suites/metricbeat/metricbeat_test.go b/e2e/_suites/metricbeat/metricbeat_test.go index 975cb61487..1176efcf26 100644 --- a/e2e/_suites/metricbeat/metricbeat_test.go +++ b/e2e/_suites/metricbeat/metricbeat_test.go @@ -19,6 +19,7 @@ import ( "github.com/elastic/e2e-testing/cli/services" "github.com/elastic/e2e-testing/cli/shell" "github.com/elastic/e2e-testing/e2e" + "github.com/elastic/e2e-testing/e2e/steps" log "github.com/sirupsen/logrus" "go.elastic.co/apm" ) @@ -245,7 +246,7 @@ func InitializeMetricbeatTestSuite(ctx *godog.TestSuiteContext) { "stackVersion": stackVersion, } - err := serviceManager.RunCompose(true, []string{"metricbeat"}, env) + err := serviceManager.RunCompose(suiteContext, true, []string{"metricbeat"}, env) if err != nil { log.WithFields(log.Fields{ "profile": "metricbeat", @@ -253,7 +254,7 @@ func InitializeMetricbeatTestSuite(ctx *godog.TestSuiteContext) { } minutesToBeHealthy := time.Duration(timeoutFactor) * time.Minute - healthy, err := e2e.WaitForElasticsearch(minutesToBeHealthy) + healthy, err := e2e.WaitForElasticsearch(suiteContext, minutesToBeHealthy) if !healthy { log.WithFields(log.Fields{ "error": err, @@ -262,22 +263,7 @@ func InitializeMetricbeatTestSuite(ctx *godog.TestSuiteContext) { } if elasticAPMActive { - apmServerURL := shell.GetEnv("APM_SERVER_URL", "") - if strings.HasPrefix(apmServerURL, "http://localhost") { - log.WithFields(log.Fields{ - "version": stackVersion, - }).Info("Starting local Kibana and APM Server for instrumentation") - - env["kibanaTag"] = stackVersion - env["apmServerTag"] = stackVersion - err := serviceManager.AddServicesToCompose(suiteContext, "metricbeat", []string{"kibana", "apm-server"}, env) - if err != nil { - log.WithFields(log.Fields{ - "error": err, - "env": env, - }).Warn("The APM Server and Kibana could not be started, but they are not needed by the tests. Continuing") - } - } + steps.AddAPMServicesForInstrumentation(suiteContext, "metricbeat", stackVersion, true, env) } }) @@ -287,9 +273,20 @@ func InitializeMetricbeatTestSuite(ctx *godog.TestSuiteContext) { } defer f() + // instrumentation + var suiteTx *apm.Transaction + var suiteParentSpan *apm.Span + var suiteContext = context.Background() + defer apm.DefaultTracer.Flush(nil) + suiteTx = apm.DefaultTracer.StartTransaction("Tear Down Metricbeat", "test.suite") + defer suiteTx.End() + suiteParentSpan = suiteTx.StartSpan("After Metricbeat test suite", "test.suite.after", nil) + suiteContext = apm.ContextWithSpan(suiteContext, suiteParentSpan) + defer suiteParentSpan.End() + if !developerMode { serviceManager := services.NewServiceManager() - err := serviceManager.StopCompose(true, []string{"metricbeat"}) + err := serviceManager.StopCompose(suiteContext, true, []string{"metricbeat"}) if err != nil { log.WithFields(log.Fields{ "profile": "metricbeat", diff --git a/e2e/elasticsearch.go b/e2e/elasticsearch.go index f662d56646..07eedda924 100644 --- a/e2e/elasticsearch.go +++ b/e2e/elasticsearch.go @@ -193,13 +193,13 @@ func search(ctx context.Context, indexName string, query map[string]interface{}) // WaitForElasticsearch waits for elasticsearch running in localhost:9200 to be healthy, returning false // if elasticsearch does not get healthy status in a defined number of minutes. -func WaitForElasticsearch(maxTimeoutMinutes time.Duration) (bool, error) { - return WaitForElasticsearchFromHostPort("localhost", 9200, maxTimeoutMinutes) +func WaitForElasticsearch(ctx context.Context, maxTimeoutMinutes time.Duration) (bool, error) { + return WaitForElasticsearchFromHostPort(ctx, "localhost", 9200, maxTimeoutMinutes) } // WaitForElasticsearchFromHostPort waits for an elasticsearch running in a host:port to be healthy, returning false // if elasticsearch does not get healthy status in a defined number of minutes. -func WaitForElasticsearchFromHostPort(host string, port int, maxTimeoutMinutes time.Duration) (bool, error) { +func WaitForElasticsearchFromHostPort(ctx context.Context, host string, port int, maxTimeoutMinutes time.Duration) (bool, error) { exp := GetExponentialBackOff(maxTimeoutMinutes) retryCount := 1 @@ -214,6 +214,11 @@ func WaitForElasticsearchFromHostPort(host string, port int, maxTimeoutMinutes t return err } + span, _ := apm.StartSpanOptions(ctx, "Health", "elasticsearch.health", apm.SpanOptions{ + Parent: apm.SpanFromContext(ctx).TraceContext(), + }) + defer span.End() + if _, err := esClient.Cluster.Health(); err != nil { log.WithFields(log.Fields{ "error": err, diff --git a/e2e/steps/befores.go b/e2e/steps/befores.go new file mode 100644 index 0000000000..742c5a0811 --- /dev/null +++ b/e2e/steps/befores.go @@ -0,0 +1,41 @@ +package steps + +import ( + "context" + "strings" + + "github.com/elastic/e2e-testing/cli/services" + "github.com/elastic/e2e-testing/cli/shell" + log "github.com/sirupsen/logrus" +) + +// AddAPMServicesForInstrumentation adds a Kibana and APM Server instances to the running project +func AddAPMServicesForInstrumentation(ctx context.Context, profile string, stackVersion string, needsKibana bool, env map[string]string) { + serviceManager := services.NewServiceManager() + + apmServerURL := shell.GetEnv("APM_SERVER_URL", "") + if strings.HasPrefix(apmServerURL, "http://localhost") { + apmServices := []string{ + "apm-server", + } + + if needsKibana { + env["kibanaTag"] = stackVersion + apmServices = append(apmServices, "kibana") + } + + log.WithFields(log.Fields{ + "services": apmServices, + "version": stackVersion, + }).Info("Starting local APM services for instrumentation") + + env["apmServerTag"] = stackVersion + err := serviceManager.AddServicesToCompose(ctx, profile, apmServices, env) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "env": env, + }).Warn("The local APM Server and Kibana could not be started, but they are not needed by the tests. Continuing") + } + } +}