From 95c21ea0359110343850f38de47ae5164195d146 Mon Sep 17 00:00:00 2001 From: Edoardo Tenani <526307+endorama@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:35:45 +0100 Subject: [PATCH] functionaltests: add first test case (#14935) Implement first test verifying upgrade from 8.15.4 to 8.16.0 for a fresh Elastic Cloud deployment. Provides a new folder structure and helpers to quickly implement functional tests for APM Server on Elastic Cloud. --------- Co-authored-by: Victor Martinez (cherry picked from commit da158f01b9e233099528d0813235cde7b7f026fe) --- .github/workflows/functional-tests.yml | 66 ++++++ functionaltests/8_15_test.go | 152 ++++++++++++++ .../TestUpgrade_8_15_4_to_8_16_0/main.tf | 18 ++ .../TestUpgrade_8_15_4_to_8_16_0/outputs.tf | 19 ++ .../TestUpgrade_8_15_4_to_8_16_0/tags.tf | 20 ++ .../TestUpgrade_8_15_4_to_8_16_0/terraform.tf | 21 ++ .../TestUpgrade_8_15_4_to_8_16_0/vars.tf | 47 +++++ functionaltests/go.mod | 41 ++++ functionaltests/go.sum | 121 +++++++++++ functionaltests/internal/esclient/client.go | 190 ++++++++++++++++++ functionaltests/internal/esclient/config.go | 54 +++++ functionaltests/internal/gen/generator.go | 128 ++++++++++++ functionaltests/internal/terraform/logger.go | 33 +++ functionaltests/internal/terraform/runner.go | 92 +++++++++ functionaltests/internal/terraform/var.go | 29 +++ functionaltests/main_test.go | 118 +++++++++++ functionaltests/utils_test.go | 37 ++++ 17 files changed, 1186 insertions(+) create mode 100644 .github/workflows/functional-tests.yml create mode 100644 functionaltests/8_15_test.go create mode 100644 functionaltests/TestUpgrade_8_15_4_to_8_16_0/main.tf create mode 100644 functionaltests/TestUpgrade_8_15_4_to_8_16_0/outputs.tf create mode 100644 functionaltests/TestUpgrade_8_15_4_to_8_16_0/tags.tf create mode 100644 functionaltests/TestUpgrade_8_15_4_to_8_16_0/terraform.tf create mode 100644 functionaltests/TestUpgrade_8_15_4_to_8_16_0/vars.tf create mode 100644 functionaltests/go.mod create mode 100644 functionaltests/go.sum create mode 100644 functionaltests/internal/esclient/client.go create mode 100644 functionaltests/internal/esclient/config.go create mode 100644 functionaltests/internal/gen/generator.go create mode 100644 functionaltests/internal/terraform/logger.go create mode 100644 functionaltests/internal/terraform/runner.go create mode 100644 functionaltests/internal/terraform/var.go create mode 100644 functionaltests/main_test.go create mode 100644 functionaltests/utils_test.go diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml new file mode 100644 index 00000000000..9ea70b02991 --- /dev/null +++ b/.github/workflows/functional-tests.yml @@ -0,0 +1,66 @@ +--- +name: functional-tests + +on: + workflow_dispatch: ~ + schedule: + - cron: '0 3 * * 1-5' + +permissions: + contents: read + id-token: write + +env: + TF_VAR_BRANCH: ${{ github.ref_name }} + TF_VAR_BUILD_ID: ${{ github.run_id }} + TF_VAR_ENVIRONMENT: 'ci' + TF_VAR_REPO: ${{ github.repository }} + TERRAFORM_VERSION: 1.10.2 + +jobs: + run: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: + - 'qa' + - 'pro' + steps: + - uses: actions/checkout@v4 + + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: "${{ env.TERRAFORM_VERSION }}" + + - uses: actions/setup-go@v5 + with: + go-version-file: 'functionaltests/go.mod' + + - uses: elastic/oblt-actions/google/auth@v1 + + - uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2.2.2 + with: + export_to_environment: true + secrets: |- + EC_API_KEY:elastic-observability/elastic-cloud-observability-team-${{ matrix.environment }}-api-key + + - run: | + export TF_VAR_CREATED_DATE=$(date +%s) + cd functionaltests && go test -v -timeout=20m -target "${{ matrix.environment }}" ./ + + notify: + if: always() + runs-on: ubuntu-latest + needs: + - run + steps: + - id: check + uses: elastic/oblt-actions/check-dependent-jobs@v1 + with: + jobs: ${{ toJSON(needs) }} + - uses: elastic/oblt-actions/slack/notify-result@v1 + with: + bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + channel-id: "#apm-server" + status: ${{ steps.check.outputs.status }} diff --git a/functionaltests/8_15_test.go b/functionaltests/8_15_test.go new file mode 100644 index 00000000000..efc3c07b162 --- /dev/null +++ b/functionaltests/8_15_test.go @@ -0,0 +1,152 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package functionaltests + +import ( + "context" + "testing" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + + "github.com/elastic/apm-server/functionaltests/internal/esclient" + "github.com/elastic/apm-server/functionaltests/internal/gen" + "github.com/elastic/apm-server/functionaltests/internal/terraform" + "github.com/elastic/go-elasticsearch/v8/typedapi/types" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpgrade_8_15_4_to_8_16_0(t *testing.T) { + ecAPICheck(t) + + start := time.Now() + ctx := context.Background() + + t.Log("creating deploment with terraform") + tf, err := terraform.New(t, t.Name()) + require.NoError(t, err) + ecTarget := terraform.Var("ec_target", *target) + ecRegion := terraform.Var("ec_region", regionFrom(*target)) + version := terraform.Var("stack_version", "8.15.4") + name := terraform.Var("name", t.Name()) + require.NoError(t, tf.Apply(ctx, ecTarget, ecRegion, version, name)) + t.Logf("time elapsed: %s", time.Now().Sub(start)) + + t.Cleanup(func() { + if !t.Failed() || (t.Failed() && *cleanupOnFailure) { + t.Log("cleanup terraform resources") + require.NoError(t, tf.Destroy(ctx, ecTarget, ecRegion, name, version)) + } else { + t.Log("test failed and cleanup-on-failure is false, skipping cleanup") + } + }) + + var escfg esclient.Config + tf.Output("apm_url", &escfg.APMServerURL) + tf.Output("es_url", &escfg.ElasticsearchURL) + tf.Output("username", &escfg.Username) + tf.Output("password", &escfg.Password) + tf.Output("kb_url", &escfg.KibanaURL) + + t.Logf("created deployment %s", escfg.KibanaURL) + + ecc, err := esclient.New(escfg) + require.NoError(t, err) + + t.Log("creating APM API key") + apikey, err := ecc.CreateAPMAPIKey(ctx, t.Name()) + require.NoError(t, err) + + g := gen.New(escfg.APMServerURL, apikey) + g.Logger = zaptest.NewLogger(t, zaptest.Level(zap.InfoLevel)) + + previous, err := getDocsCountPerDS(t, ctx, ecc) + require.NoError(t, err) + + g.RunBlockingWait(ctx, ecc, expectedIngestForASingleRun(), previous, 1*time.Minute) + + beforeUpgradeCount, err := getDocsCountPerDS(t, ctx, ecc) + require.NoError(t, err) + assertDocCount(t, beforeUpgradeCount, previous, expectedIngestForASingleRun()) + + t.Log("check data streams") + var dss []types.DataStream + dss, err = ecc.GetDataStream(ctx, "*apm*") + require.NoError(t, err) + assertDatastreams(t, checkDatastreamWant{ + Quantity: 8, + PreferIlm: false, + DSManagedBy: "Data stream lifecycle", + IndicesPerDs: 1, + IndicesManagedBy: []string{"Data stream lifecycle"}, + }, dss) + t.Logf("time elapsed: %s", time.Now().Sub(start)) + + t.Log("upgrade to 8.16.0") + require.NoError(t, tf.Apply(ctx, ecTarget, ecRegion, name, terraform.Var("stack_version", "8.16.0"))) + t.Logf("time elapsed: %s", time.Now().Sub(start)) + + t.Log("check number of documents after upgrade") + afterUpgradeCount, err := getDocsCountPerDS(t, ctx, ecc) + require.NoError(t, err) + // We assert that no changes happened in the number of documents after upgrade + // to ensure the state didn't change before running the next ingestion round + // and further assertions. + // We don't expect any change here unless something broke during the upgrade. + assertDocCount(t, afterUpgradeCount, esclient.APMDataStreamsDocCount{}, beforeUpgradeCount) + + t.Log("check data streams after upgrade, no rollover expected") + dss, err = ecc.GetDataStream(ctx, "*apm*") + require.NoError(t, err) + assertDatastreams(t, checkDatastreamWant{ + Quantity: 8, + PreferIlm: false, + DSManagedBy: "Data stream lifecycle", + IndicesPerDs: 1, + IndicesManagedBy: []string{"Data stream lifecycle"}, + }, dss) + + g.RunBlockingWait(ctx, ecc, expectedIngestForASingleRun(), previous, 1*time.Minute) + + t.Log("check number of documents") + afterUpgradeIngestionCount, err := getDocsCountPerDS(t, ctx, ecc) + require.NoError(t, err) + assertDocCount(t, afterUpgradeIngestionCount, afterUpgradeCount, expectedIngestForASingleRun()) + + // Confirm datastreams are + // v managed by DSL if created after 8.15.0 + // x managed by ILM if created before 8.15.0 + t.Log("check data streams and verify lazy rollover happened") + dss2, err := ecc.GetDataStream(ctx, "*apm*") + require.NoError(t, err) + assertDatastreams(t, checkDatastreamWant{ + Quantity: 8, + PreferIlm: false, + DSManagedBy: "Data stream lifecycle", + IndicesPerDs: 2, + IndicesManagedBy: []string{"Data stream lifecycle", "Data stream lifecycle"}, + }, dss2) + t.Logf("time elapsed: %s", time.Now().Sub(start)) + + res, err := ecc.GetESErrorLogs(ctx) + require.NoError(t, err) + assert.Zero(t, res.Hits.Total.Value) +} diff --git a/functionaltests/TestUpgrade_8_15_4_to_8_16_0/main.tf b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/main.tf new file mode 100644 index 00000000000..c2b992ac9ac --- /dev/null +++ b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/main.tf @@ -0,0 +1,18 @@ +module "ec_deployment" { + source = "../../testing/infra/terraform/modules/ec_deployment" + region = var.ec_region + + deployment_template = "aws-storage-optimized" + deployment_name_prefix = var.name + + // self monitoring is enabled so we can inspect Elasticsearch + // logs from tests. + observability_deployment = "self" + + apm_server_size = "1g" + + elasticsearch_size = "4g" + elasticsearch_zone_count = 1 + + stack_version = var.stack_version +} diff --git a/functionaltests/TestUpgrade_8_15_4_to_8_16_0/outputs.tf b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/outputs.tf new file mode 100644 index 00000000000..44797bbe221 --- /dev/null +++ b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/outputs.tf @@ -0,0 +1,19 @@ +output "apm_url" { + value = module.ec_deployment.apm_url +} + +output "es_url" { + value = module.ec_deployment.elasticsearch_url +} + +output "username" { + value = module.ec_deployment.elasticsearch_username +} +output "password" { + value = module.ec_deployment.elasticsearch_password + sensitive = true +} + +output "kb_url" { + value = module.ec_deployment.kibana_url +} diff --git a/functionaltests/TestUpgrade_8_15_4_to_8_16_0/tags.tf b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/tags.tf new file mode 100644 index 00000000000..f59920ac32d --- /dev/null +++ b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/tags.tf @@ -0,0 +1,20 @@ +resource "time_static" "created_date" {} + +locals { + ci_tags = { + environment = var.ENVIRONMENT + repo = var.REPO + branch = var.BRANCH + build = var.BUILD_ID + created_date = coalesce(var.CREATED_DATE, time_static.created_date.unix) + } + project = "apm-server-functionaltest" +} + +module "tags" { + source = "../../testing/infra/terraform/modules/tags" + # use the convention for team/shared owned resources if we are running in CI. + # assume this is an individually owned resource otherwise. + project = local.project +} + diff --git a/functionaltests/TestUpgrade_8_15_4_to_8_16_0/terraform.tf b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/terraform.tf new file mode 100644 index 00000000000..8221f99e424 --- /dev/null +++ b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/terraform.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 0.12.29" + + required_providers { + ec = { + source = "elastic/ec" + version = "0.5.1" + } + } +} + +locals { + api_endpoints = { + qa = "https://public-api.qa.cld.elstc.co" + pro = "https://api.elastic-cloud.com" + } +} + +provider "ec" { + endpoint = local.api_endpoints[var.ec_target] +} diff --git a/functionaltests/TestUpgrade_8_15_4_to_8_16_0/vars.tf b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/vars.tf new file mode 100644 index 00000000000..a1379c5bd41 --- /dev/null +++ b/functionaltests/TestUpgrade_8_15_4_to_8_16_0/vars.tf @@ -0,0 +1,47 @@ +variable "ec_target" { + type = string + description = "The Elastic Cloud environment to target" + validation { + condition = contains(["qa", "pro"], var.ec_target) + error_message = "Valid values are (qa, pro)." + } +} + +variable "ec_region" { + type = string + description = "The Elastic Cloud region to target" +} + +variable "name" { + type = string + description = "The deployment name" +} + +variable "stack_version" { + type = string + description = "The Elasticsearch version to bootstrap" +} + +# CI variables +variable "BRANCH" { + description = "Branch name or pull request for tagging purposes" + default = "unknown-branch" +} + +variable "BUILD_ID" { + description = "Build ID in the CI for tagging purposes" + default = "unknown-build" +} + +variable "CREATED_DATE" { + description = "Creation date in epoch time for tagging purposes" + default = "" +} + +variable "ENVIRONMENT" { + default = "unknown-environment" +} + +variable "REPO" { + default = "unknown-repo-name" +} diff --git a/functionaltests/go.mod b/functionaltests/go.mod new file mode 100644 index 00000000000..e2cb5f8b0a6 --- /dev/null +++ b/functionaltests/go.mod @@ -0,0 +1,41 @@ +module github.com/elastic/apm-server/functionaltests + +go 1.23.2 + +require ( + github.com/elastic/apm-perf v0.0.0-20241230130730-2ad47482b731 + github.com/elastic/go-elasticsearch/v8 v8.16.0 + github.com/hashicorp/terraform-exec v0.21.0 + github.com/stretchr/testify v1.10.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/elastic/elastic-transport-go/v8 v8.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/terraform-json v0.22.1 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + go.elastic.co/apm/v2 v2.6.2 // indirect + go.elastic.co/fastjson v1.4.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.8.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/functionaltests/go.sum b/functionaltests/go.sum new file mode 100644 index 00000000000..5b92726e0bf --- /dev/null +++ b/functionaltests/go.sum @@ -0,0 +1,121 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elastic/apm-perf v0.0.0-20241213094810-d1d7602614f5 h1:W7I6yPe/DqVHQaQ7SL9l7jBRYJQ/gF7wDgQqA7/ihCE= +github.com/elastic/apm-perf v0.0.0-20241213094810-d1d7602614f5/go.mod h1:Q1Tm237o37q6gJpYZ99Vpk8zlGaSenuiJmcE1uu9VLc= +github.com/elastic/apm-perf v0.0.0-20241230130730-2ad47482b731 h1:w9a+mWNmzgAUEbll5ntXB145YMYS9YNPqpm9SPkdKDY= +github.com/elastic/apm-perf v0.0.0-20241230130730-2ad47482b731/go.mod h1:9q0PpMw5QPD96OPD2Emay89Fog/jACbYjy6sWoSqEc4= +github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA= +github.com/elastic/elastic-transport-go/v8 v8.6.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= +github.com/elastic/go-elasticsearch/v8 v8.16.0 h1:f7bR+iBz8GTAVhwyFO3hm4ixsz2eMaEy0QroYnXV3jE= +github.com/elastic/go-elasticsearch/v8 v8.16.0/go.mod h1:lGMlgKIbYoRvay3xWBeKahAiJOgmFDsjZC39nmO3H64= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9Km8e0= +github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA= +github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= +github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= +github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= +github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.elastic.co/apm/v2 v2.6.2 h1:VBplAxgbOgTv+Giw/FS91xJpHYw/q8fz/XKPvqC+7/o= +go.elastic.co/apm/v2 v2.6.2/go.mod h1:33rOXgtHwbgZcDgi6I/GtCSMZQqgxkHC0IQT3gudKvo= +go.elastic.co/fastjson v1.4.0 h1:a4BXUKXZHAzjVOPrqtEx2FDsIRBCMek01vCnrtyutWs= +go.elastic.co/fastjson v1.4.0/go.mod h1:ZD5um63l0/8TIdddZbL2znD83FAr2IckYa3KR7VcdNA= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/functionaltests/internal/esclient/client.go b/functionaltests/internal/esclient/client.go new file mode 100644 index 00000000000..76b7028310e --- /dev/null +++ b/functionaltests/internal/esclient/client.go @@ -0,0 +1,190 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package esclient + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/elastic/go-elasticsearch/v8" + "github.com/elastic/go-elasticsearch/v8/typedapi/core/search" + "github.com/elastic/go-elasticsearch/v8/typedapi/esql/query" + "github.com/elastic/go-elasticsearch/v8/typedapi/security/createapikey" + "github.com/elastic/go-elasticsearch/v8/typedapi/types" +) + +type Client struct { + es *elasticsearch.TypedClient +} + +// New returns a new Client for querying APM data. +func New(cfg Config) (*Client, error) { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: cfg.TLSSkipVerify} + + es, err := elasticsearch.NewTypedClient(elasticsearch.Config{ + Addresses: []string{cfg.ElasticsearchURL}, + Username: cfg.Username, + APIKey: cfg.APIKey, + Password: cfg.Password, + Transport: transport, + }) + if err != nil { + return nil, fmt.Errorf("error creating Elasticsearch client: %w", err) + } + return &Client{ + es: es, + }, nil +} + +var elasticsearchTimeUnits = []struct { + Duration time.Duration + Unit string +}{ + {time.Hour, "h"}, + {time.Minute, "m"}, + {time.Second, "s"}, + {time.Millisecond, "ms"}, + {time.Microsecond, "micros"}, +} + +// formatDurationElasticsearch formats a duration using +// Elasticsearch supported time units. +// +// See https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#time-units +func formatDurationElasticsearch(d time.Duration) string { + for _, tu := range elasticsearchTimeUnits { + if d%tu.Duration == 0 { + return fmt.Sprintf("%d%s", d/tu.Duration, tu.Unit) + } + } + return fmt.Sprintf("%dnanos", d) +} + +// CreateAgentAPIKey creates an agent API Key, and returns it in the +// base64-encoded form that agents should provide. +// +// If expiration is less than or equal to zero, then the API Key never expires. +func (c *Client) CreateAPIKey(ctx context.Context, name string, expiration time.Duration, roles map[string]types.RoleDescriptor) (string, error) { + var maybeExpiration types.Duration + if expiration > 0 { + maybeExpiration = formatDurationElasticsearch(expiration) + } + resp, err := c.es.Security.CreateApiKey().Request(&createapikey.Request{ + Name: &name, + Expiration: maybeExpiration, + RoleDescriptors: roles, + Metadata: map[string]json.RawMessage{ + "creator": []byte(`"apmclient"`), + }, + }).Do(ctx) + if err != nil { + return "", fmt.Errorf("error creating API Key: %w", err) + } + return resp.Encoded, nil +} + +func (c *Client) CreateAPMAPIKey(ctx context.Context, name string) (string, error) { + return c.CreateAPIKey(context.Background(), + name, -1, map[string]types.RoleDescriptor{}, + ) +} + +func (c *Client) GetDataStream(ctx context.Context, name string) ([]types.DataStream, error) { + resp, err := c.es.Indices.GetDataStream().Name(name).Do(ctx) + if err != nil { + return []types.DataStream{}, fmt.Errorf("cannot GET datastream: %w", err) + } + + return resp.DataStreams, nil +} + +// ApmDocCount is used to unmarshal response from ES|QL query in ApmDocCount(). +type ApmDocCount struct { + Count int + Datastream string +} + +// APMDataStreamsDocCount is an easy to assert on format reporting doc count for +// APM data streams. +type APMDataStreamsDocCount map[string]int + +func (c *Client) ApmDocCount(ctx context.Context) (APMDataStreamsDocCount, error) { + q := `FROM traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-* +| EVAL datastream = CONCAT(data_stream.type, "-", data_stream.dataset, "-", data_stream.namespace) +| STATS count = COUNT(*) BY datastream +| SORT count DESC` + + qry := c.es.Esql.Query().Query(q) + resp, err := query.Helper[ApmDocCount](ctx, qry) + if err != nil { + var eserr *types.ElasticsearchError + // suppress this error as it only indicates no data is available yet. + expected := `Found 1 problem +line 1:1: Unknown index [traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*]` + if errors.As(err, &eserr) && + eserr.ErrorCause.Reason != nil && + *eserr.ErrorCause.Reason == expected { + return APMDataStreamsDocCount{}, nil + } + + return APMDataStreamsDocCount{}, fmt.Errorf("cannot retrieve APM doc count: %w", err) + } + + res := APMDataStreamsDocCount{} + for _, dc := range resp { + res[dc.Datastream] = dc.Count + } + + return res, nil +} + +// GetESErrorLogs retrieves Elasticsearch error logs. +// The search query is on the Index used by Elasticsearch monitoring to store logs. +func (c *Client) GetESErrorLogs(ctx context.Context) (*search.Response, error) { + res, err := c.es.Search(). + Index("elastic-cloud-logs-8"). + Request(&search.Request{ + Query: &types.Query{ + Bool: &types.BoolQuery{ + Must: []types.Query{ + { + Match: map[string]types.MatchQuery{ + "service.type": {Query: "elasticsearch"}, + }, + }, + { + Match: map[string]types.MatchQuery{ + "log.level": {Query: "ERROR"}, + }, + }, + }, + }, + }, + }).Do(ctx) + if err != nil { + return search.NewResponse(), fmt.Errorf("cannot run search query: %w", err) + } + + return res, nil +} diff --git a/functionaltests/internal/esclient/config.go b/functionaltests/internal/esclient/config.go new file mode 100644 index 00000000000..b987a1c34a9 --- /dev/null +++ b/functionaltests/internal/esclient/config.go @@ -0,0 +1,54 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package esclient + +type Config struct { + // ElasticsearchURL holds the Elasticsearch URL. + ElasticsearchURL string + + // Username holds the Elasticsearch username for basic auth. + Username string + + // Password holds the Elasticsearch password for basic auth. + Password string + + // APIKey holds an Elasticsearch API Key. + // + // This will be set from $ELASTICSEARCH_API_KEY if specified. + APIKey string + + // APMServerURL holds the APM Server URL. + // + // If this is unspecified, it will be derived from + // ElasticsearchURL if that is an Elastic Cloud URL. + APMServerURL string + + // KibanaURL holds the Kibana URL. + // + // If this is unspecified, it will be derived from + // ElasticsearchURL if that is an Elastic Cloud URL. + KibanaURL string + + // TLSSkipVerify determines if TLS certificate + // verification is skipped or not. Default to false. + // + // If not specified the value will be take from + // TLS_SKIP_VERIFY env var. + // Any value different from "" is considered true. + TLSSkipVerify bool +} diff --git a/functionaltests/internal/gen/generator.go b/functionaltests/internal/gen/generator.go new file mode 100644 index 00000000000..d5709927552 --- /dev/null +++ b/functionaltests/internal/gen/generator.go @@ -0,0 +1,128 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package gen + +import ( + "context" + "fmt" + "net/url" + "time" + + "go.uber.org/zap" + + "github.com/elastic/apm-perf/pkg/telemetrygen" + "github.com/elastic/apm-server/functionaltests/internal/esclient" +) + +type Generator struct { + Logger *zap.Logger + APMAPIKey string + APMServerURL string + EventRate string +} + +func New(url, apikey string) *Generator { + return &Generator{ + Logger: zap.NewNop(), + APMAPIKey: apikey, + APMServerURL: url, + EventRate: "1000/s", + } +} + +// RunBlocking runs the underlying generator in blocking mode. +func (g *Generator) RunBlocking(ctx context.Context) error { + cfg := telemetrygen.DefaultConfig() + cfg.APIKey = g.APMAPIKey + + u, err := url.Parse(g.APMServerURL) + if err != nil { + return fmt.Errorf("cannot parse APM server URL: %w", err) + } + cfg.ServerURL = u + + cfg.EventRate.Set(g.EventRate) + gen, err := telemetrygen.New(cfg) + if err != nil { + return fmt.Errorf("cannot create telemetrygen Generator: %w", err) + } + + g.Logger.Info("ingest data") + gen.Logger = g.Logger + return gen.RunBlocking(ctx) +} + +// RunBlockingWait runs the underlying generator in blocking mode and waits until the +// document count retrieved through ecc matches expected or timeout. It supports +// specifying previous document count to offset the expectation based on a previous state. +// expected and previous must be maps of . +func (g *Generator) RunBlockingWait(ctx context.Context, ecc *esclient.Client, expected, previous map[string]int, timeout time.Duration) error { + if err := g.RunBlocking(ctx); err != nil { + return fmt.Errorf("cannot run generator: %w", err) + } + + // this function checks that expected docs count is reached, + // accounting for any previous state. + checkDocsCount := func(docsCount map[string]int) bool { + equal := false + for ds, c := range docsCount { + if e, ok := expected[ds]; ok { + got := c - previous[ds] + equal = (e == got) + } + } + return equal + } + prevdocs := map[string]int{} + equaltoprevdocs := 0 + // this function checks that all returned data streams doc counts + // stay still for some iterations. This forces a 15 seconds longer wait + // but ensures that aggredation data streams have stabilized before + // allowing the code to proceed. This should prevent situation where + // aggregation are still running when expected data streams reached + // their expected doc count. + checkAggregationDocs := func(prevdocs, docsCount map[string]int) bool { + if equaltoprevdocs == 3 { + return true + } + if fmt.Sprint(prevdocs) == fmt.Sprint(docsCount) { + equaltoprevdocs += 1 + } + return false + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + tctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + for { + select { + case <-tctx.Done(): + return nil + case <-ticker.C: + docsCount, err := ecc.ApmDocCount(ctx) + if err != nil { + return fmt.Errorf("cannot retrieve APM doc count: %w", err) + } + if checkDocsCount(docsCount) && checkAggregationDocs(prevdocs, docsCount) { + return nil + } + prevdocs = docsCount + } + } +} diff --git a/functionaltests/internal/terraform/logger.go b/functionaltests/internal/terraform/logger.go new file mode 100644 index 00000000000..c5f56005b12 --- /dev/null +++ b/functionaltests/internal/terraform/logger.go @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package terraform + +import ( + "testing" +) + +// tfLoggerv2 wraps a testing.TB to implement the printfer interface +// required by terraform-exec logger. +type tfLoggerv2 struct { + testing.TB +} + +// Printf implements terraform-exec.printfer interface +func (l *tfLoggerv2) Printf(format string, v ...interface{}) { + l.Logf(format, v...) +} diff --git a/functionaltests/internal/terraform/runner.go b/functionaltests/internal/terraform/runner.go new file mode 100644 index 00000000000..4d88216003c --- /dev/null +++ b/functionaltests/internal/terraform/runner.go @@ -0,0 +1,92 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package terraform + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +type Runner struct { + initialized bool + outputs map[string]tfexec.OutputMeta + tf *tfexec.Terraform +} + +func New(t *testing.T, workingDir string) (*Runner, error) { + tr := Runner{} + + tf, err := tfexec.NewTerraform(workingDir, "terraform") + if err != nil { + return &tr, fmt.Errorf("error instantiating terraform runner: %w", err) + } + tf.SetLogger(&tfLoggerv2{t}) + tr.tf = tf + if err := tr.init(); err != nil { + return &tr, fmt.Errorf("cannot run terraform init: %w", err) + } else { + tr.initialized = true + } + + return &tr, nil +} + +func (t *Runner) init() error { + return t.tf.Init(context.Background(), tfexec.Upgrade(true)) +} + +func (t *Runner) Apply(ctx context.Context, vars ...tfexec.ApplyOption) error { + if !t.initialized { + if err := t.init(); err != nil { + return fmt.Errorf("cannot init before apply: %w", err) + } + } + if err := t.tf.Apply(ctx, vars...); err != nil { + return fmt.Errorf("cannot apply: %w", err) + } + + output, err := t.tf.Output(ctx) + if err != nil { + return fmt.Errorf("cannot run terraform output: %w", err) + } + + t.outputs = output + return nil +} + +func (t *Runner) Destroy(ctx context.Context, vars ...tfexec.DestroyOption) error { + if !t.initialized { + if err := t.init(); err != nil { + return fmt.Errorf("cannot init before apply: %w", err) + } + } + + return t.tf.Destroy(ctx, vars...) +} + +func (t *Runner) Output(name string, res any) error { + o := t.outputs[name] + if err := json.Unmarshal(o.Value, res); err != nil { + return fmt.Errorf("cannot unmarshal output: %w", err) + } + return nil +} diff --git a/functionaltests/internal/terraform/var.go b/functionaltests/internal/terraform/var.go new file mode 100644 index 00000000000..a31c94f6963 --- /dev/null +++ b/functionaltests/internal/terraform/var.go @@ -0,0 +1,29 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +// Var is a helper to simplify creating Terraform vars to pass to terraform-exec. +func Var(name, value string) *tfexec.VarOption { + return tfexec.Var(fmt.Sprintf("%s=%s", name, value)) +} diff --git a/functionaltests/main_test.go b/functionaltests/main_test.go new file mode 100644 index 00000000000..796063ae920 --- /dev/null +++ b/functionaltests/main_test.go @@ -0,0 +1,118 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package functionaltests + +import ( + "context" + "flag" + "fmt" + "testing" + + "github.com/elastic/apm-server/functionaltests/internal/esclient" + "github.com/elastic/go-elasticsearch/v8/typedapi/types" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var cleanupOnFailure *bool = flag.Bool("cleanup-on-failure", true, "Whether to run cleanup even if the test failed.") + +// target is the Elastic Cloud environment to target with these test. +// We use 'pro' for production as that is the key used to retrieve EC_API_KEY from secret storage. +var target *string = flag.String("target", "pro", "The target environment where to run tests againts. Valid values are: qa, pro") + +// expectedIngestForASingleRun() represent the expected number of ingested document after a +// single run of ingest(). +// Only non aggregation data streams are included, as aggregation ones differs on different +// runs. +func expectedIngestForASingleRun() esclient.APMDataStreamsDocCount { + return map[string]int{ + "traces-apm-default": 15013, + "metrics-apm.app.opbeans_python-default": 1437, + "metrics-apm.internal-default": 1351, + "logs-apm.error-default": 364, + } +} + +// getDocsCountPerDS retrieves document count. +func getDocsCountPerDS(t *testing.T, ctx context.Context, ecc *esclient.Client) (esclient.APMDataStreamsDocCount, error) { + t.Helper() + return ecc.ApmDocCount(ctx) +} + +// assertDocCount check if specified document count is equal to expected minus +// documents count from a previous state. +func assertDocCount(t *testing.T, docsCount, previous, expected esclient.APMDataStreamsDocCount) { + t.Helper() + for ds, v := range docsCount { + if e, ok := expected[ds]; ok { + assert.Equal(t, e, v-previous[ds], + fmt.Sprintf("wrong document count for %s", ds)) + } + } +} + +type checkDatastreamWant struct { + Quantity int + DSManagedBy string + IndicesPerDs int + PreferIlm bool + IndicesManagedBy []string +} + +// assertDatastreams assert expected values on specific data streams in a cluster. +func assertDatastreams(t *testing.T, expected checkDatastreamWant, actual []types.DataStream) { + t.Helper() + + require.Len(t, actual, expected.Quantity, "number of APM datastream differs from expectations") + for _, v := range actual { + if expected.PreferIlm { + assert.True(t, v.PreferIlm, "datastream %s should prefer ILM", v.Name) + } else { + assert.False(t, v.PreferIlm, "datastream %s should not prefer ILM", v.Name) + } + + assert.Equal(t, expected.DSManagedBy, v.NextGenerationManagedBy.Name, + `datastream %s should be managed by "%s"`, v.Name, expected.DSManagedBy, + ) + assert.Len(t, v.Indices, expected.IndicesPerDs, + "datastream %s should have %d indices", v.Name, expected.IndicesPerDs, + ) + for i, index := range v.Indices { + assert.Equal(t, expected.IndicesManagedBy[i], index.ManagedBy.Name, + `index %s should be managed by "%s"`, index.IndexName, + expected.IndicesManagedBy[i], + ) + } + } + +} + +// regionFrom returns the appropriate region to run test +// againts based on specified target. +// https://www.elastic.co/guide/en/cloud/current/ec-regions-templates-instances.html +func regionFrom(target string) string { + switch target { + case "qa": + return "aws-eu-west-1" + case "pro": + return "eu-west-1" + default: + panic("target value is not accepted") + } +} diff --git a/functionaltests/utils_test.go b/functionaltests/utils_test.go new file mode 100644 index 00000000000..83f5086331f --- /dev/null +++ b/functionaltests/utils_test.go @@ -0,0 +1,37 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package functionaltests + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// ecAPICheck verifies if EC_API_KEY env var is set. +// This is a simple check to alert users if this necessary env var +// is not available. +// +// Functional tests are expected to run Terraform code to operate +// on infrastructure required for each tests and to query Elastic +// Cloud APIs. In both cases a valid API key is required. +func ecAPICheck(t *testing.T) { + t.Helper() + require.NotEmpty(t, os.Getenv("EC_API_KEY"), "EC_API_KEY env var not set") +}