diff --git a/.buildkite/scripts/steps/integration_tests.sh b/.buildkite/scripts/steps/integration_tests.sh index 995f5018a14..ee002df1a48 100755 --- a/.buildkite/scripts/steps/integration_tests.sh +++ b/.buildkite/scripts/steps/integration_tests.sh @@ -9,7 +9,7 @@ DEV=true EXTERNAL=true SNAPSHOT=true PLATFORMS=linux/amd64,linux/arm64 PACKAGES= # Run integration tests set +e # Use 8.10.2-SNAPSHOT until the first 8.10.3-SNAPSHOT is produced. -AGENT_STACK_VERSION="8.10.2-SNAPSHOT" TEST_INTEG_CLEAN_ON_EXIT=true SNAPSHOT=true mage integration:test +AGENT_STACK_VERSION="8.10.2-SNAPSHOT" TEST_INTEG_AUTH_ESS_REGION=azure-eastus2 TEST_INTEG_CLEAN_ON_EXIT=true SNAPSHOT=true mage integration:test TESTS_EXIT_STATUS=$? set -e diff --git a/NOTICE.txt b/NOTICE.txt index e33c9a36394..4a48afc5a6c 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -17274,11 +17274,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : golang.org/x/oauth2 -Version: v0.4.0 +Version: v0.7.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/oauth2@v0.4.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/oauth2@v0.7.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. diff --git a/changelog/8.10.2.asciidoc b/changelog/8.10.2.asciidoc new file mode 100644 index 00000000000..fff2f8f1208 --- /dev/null +++ b/changelog/8.10.2.asciidoc @@ -0,0 +1,31 @@ +// begin 8.10.2 relnotes + +[[release-notes-8.10.2]] +== 8.10.2 + +Review important information about the 8.10.2 release. + +[discrete] +[[security-updates-8.10.2]] +=== Security updates + + +elastic-agent:: + +* Upgrade Go version to 1.20.8. {elastic-agent-pull}https://github.com/elastic/elastic-agent/pull/3393[#https://github.com/elastic/elastic-agent/pull/3393] + + + + + + + + + + + + + + + +// end 8.10.2 relnotes diff --git a/changelog/8.10.2.yaml b/changelog/8.10.2.yaml new file mode 100644 index 00000000000..77f2e565241 --- /dev/null +++ b/changelog/8.10.2.yaml @@ -0,0 +1,13 @@ +version: 8.10.2 +entries: + - kind: security + summary: Upgrade Go version to 1.20.8. + description: "" + component: elastic-agent + pr: + - https://github.com/elastic/elastic-agent/pull/3393 + issue: [] + timestamp: 1694700200 + file: + name: 1694700200-Upgrade-to-Go-1.20.8.yaml + checksum: f9a337cf39abbf93f66c393e52c66053ea700d09 diff --git a/changelog/fragments/1694700200-Upgrade-to-Go-1.20.8.yaml b/changelog/fragments/1695035111-Resilient-handling-of-air-gapped-PGP-checks.yaml similarity index 68% rename from changelog/fragments/1694700200-Upgrade-to-Go-1.20.8.yaml rename to changelog/fragments/1695035111-Resilient-handling-of-air-gapped-PGP-checks.yaml index 211338b03db..caaa8a2f53a 100644 --- a/changelog/fragments/1694700200-Upgrade-to-Go-1.20.8.yaml +++ b/changelog/fragments/1695035111-Resilient-handling-of-air-gapped-PGP-checks.yaml @@ -8,25 +8,24 @@ # - security: impacts on the security of a product or a user’s deployment. # - upgrade: important information for someone upgrading from a prior version # - other: does not fit into any of the other categories -kind: security +kind: bug-fix # Change summary; a 80ish characters long description of the change. -summary: Upgrade to Go 1.20.8. +summary: Resilient handling of air gapped PGP checks # Long description; in case the summary is not enough to describe the change # this field accommodate a description without length limits. -# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. -#description: +description: Elastic Agent should not fail when remote PGP is specified (or official Elastic fallback PGP used) and remote is not available -# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc. -component: "elastic-agent" +# Affected component; a word indicating the component this changeset affects. +component: elastic-agent -# PR URL; optional; the PR number that added the changeset. +# PR number; optional; the PR number that added the changeset. # If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. # NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. # Please provide it if you are adding a fragment for a different PR. -pr: https://github.com/elastic/elastic-agent/pull/3393 +pr: 3427 -# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). +# Issue number; optional; the GitHub issue related to this changeset (either closes or is part of). # If not present is automatically filled by the tooling with the issue linked to the PR number. -#issue: https://github.com/owner/repo/1234 +issue: 3368 diff --git a/deploy/kubernetes/elastic-agent-managed-kubernetes.yaml b/deploy/kubernetes/elastic-agent-managed-kubernetes.yaml index 0bc34b8f0e5..fa45d93268b 100644 --- a/deploy/kubernetes/elastic-agent-managed-kubernetes.yaml +++ b/deploy/kubernetes/elastic-agent-managed-kubernetes.yaml @@ -30,7 +30,7 @@ spec: dnsPolicy: ClusterFirstWithHostNet containers: - name: elastic-agent - image: docker.elastic.co/beats/elastic-agent:8.10.1 + image: docker.elastic.co/beats/elastic-agent:8.10.2 env: # Set to 1 for enrollment into Fleet server. If not set, Elastic Agent is run in standalone mode - name: FLEET_ENROLL diff --git a/deploy/kubernetes/elastic-agent-standalone-kubernetes.yaml b/deploy/kubernetes/elastic-agent-standalone-kubernetes.yaml index fe674225dca..892cfa8b50f 100644 --- a/deploy/kubernetes/elastic-agent-standalone-kubernetes.yaml +++ b/deploy/kubernetes/elastic-agent-standalone-kubernetes.yaml @@ -683,13 +683,13 @@ spec: # - -c # - >- # mkdir -p /etc/elastic-agent/inputs.d && - # wget -O - https://github.com/elastic/elastic-agent/archive/8.10.1.tar.gz | tar xz -C /etc/elastic-agent/inputs.d --strip=5 "elastic-agent-8.10.1/deploy/kubernetes/elastic-agent-standalone/templates.d" + # wget -O - https://github.com/elastic/elastic-agent/archive/8.10.2.tar.gz | tar xz -C /etc/elastic-agent/inputs.d --strip=5 "elastic-agent-8.10.2/deploy/kubernetes/elastic-agent-standalone/templates.d" # volumeMounts: # - name: external-inputs # mountPath: /etc/elastic-agent/inputs.d containers: - name: elastic-agent-standalone - image: docker.elastic.co/beats/elastic-agent:8.10.1 + image: docker.elastic.co/beats/elastic-agent:8.10.2 args: ["-c", "/etc/elastic-agent/agent.yml", "-e"] env: # The basic authentication username used to connect to Elasticsearch diff --git a/go.mod b/go.mod index f15f05d899d..172fcc5c204 100644 --- a/go.mod +++ b/go.mod @@ -144,7 +144,7 @@ require ( golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.9.0 // indirect - golang.org/x/oauth2 v0.4.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/term v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect diff --git a/go.sum b/go.sum index 344fa811d0e..13586635f18 100644 --- a/go.sum +++ b/go.sum @@ -2059,8 +2059,9 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier.go b/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier.go index e75d25dbb7e..72066129222 100644 --- a/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier.go +++ b/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier.go @@ -124,10 +124,11 @@ func (v *Verifier) verifyAsc(fullPath string, skipDefaultPgp bool, pgpSources .. if len(check) == 0 { continue } - raw, err := download.PgpBytesFromSource(check, v.client) + raw, err := download.PgpBytesFromSource(v.log, check, v.client) if err != nil { return err } + if len(raw) == 0 { continue } diff --git a/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier_test.go b/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier_test.go index 603d997e450..5012e8244dd 100644 --- a/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier_test.go +++ b/internal/pkg/agent/application/upgrade/artifact/download/fs/verifier_test.go @@ -173,50 +173,60 @@ func prepareFetchVerifyTests(dropPath, targetDir, targetFilePath, hashTargetFile } func TestVerify(t *testing.T) { - log, _ := logger.New("", false) - targetDir, err := ioutil.TempDir(os.TempDir(), "") - if err != nil { - t.Fatal(err) + tt := []struct { + Name string + RemotePGPUris []string + UnreachableCount int + }{ + {"default", nil, 0}, + {"unreachable local path", []string{download.PgpSourceURIPrefix + "https://127.0.0.1:2874/path/does/not/exist"}, 1}, } - timeout := 30 * time.Second - - config := &artifact.Config{ - TargetDirectory: targetDir, - DropPath: filepath.Join(targetDir, "drop"), - OperatingSystem: "linux", - Architecture: "32", - HTTPTransportSettings: httpcommon.HTTPTransportSettings{ - Timeout: timeout, - }, + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + log, obs := logger.NewTesting("TestVerify") + targetDir, err := ioutil.TempDir(os.TempDir(), "") + require.NoError(t, err) + + timeout := 30 * time.Second + + config := &artifact.Config{ + TargetDirectory: targetDir, + DropPath: filepath.Join(targetDir, "drop"), + OperatingSystem: "linux", + Architecture: "32", + HTTPTransportSettings: httpcommon.HTTPTransportSettings{ + Timeout: timeout, + }, + } + + err = prepareTestCase(beatSpec, version, config) + require.NoError(t, err) + + testClient := NewDownloader(config) + artifact, err := testClient.Download(context.Background(), beatSpec, version) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(artifact) + os.Remove(artifact + ".sha512") + os.RemoveAll(config.DropPath) + }) + + _, err = os.Stat(artifact) + require.NoError(t, err) + + testVerifier, err := NewVerifier(log, config, true, nil) + require.NoError(t, err) + + err = testVerifier.Verify(beatSpec, version, false, tc.RemotePGPUris...) + require.NoError(t, err) + + // log message informing remote PGP was skipped + logs := obs.FilterMessageSnippet("Skipped remote PGP located at") + require.Equal(t, tc.UnreachableCount, logs.Len()) + }) } - - if err := prepareTestCase(beatSpec, version, config); err != nil { - t.Fatal(err) - } - - testClient := NewDownloader(config) - artifact, err := testClient.Download(context.Background(), beatSpec, version) - if err != nil { - t.Fatal(err) - } - - _, err = os.Stat(artifact) - if err != nil { - t.Fatal(err) - } - - testVerifier, err := NewVerifier(log, config, true, nil) - if err != nil { - t.Fatal(err) - } - - err = testVerifier.Verify(beatSpec, version, false) - require.NoError(t, err) - - os.Remove(artifact) - os.Remove(artifact + ".sha512") - os.RemoveAll(config.DropPath) } func prepareTestCase(a artifact.Artifact, version string, cfg *artifact.Config) error { diff --git a/internal/pkg/agent/application/upgrade/artifact/download/http/verifier.go b/internal/pkg/agent/application/upgrade/artifact/download/http/verifier.go index e708fe79e91..61992e08816 100644 --- a/internal/pkg/agent/application/upgrade/artifact/download/http/verifier.go +++ b/internal/pkg/agent/application/upgrade/artifact/download/http/verifier.go @@ -125,10 +125,11 @@ func (v *Verifier) verifyAsc(a artifact.Artifact, version string, skipDefaultPgp if len(check) == 0 { continue } - raw, err := download.PgpBytesFromSource(check, v.client) + raw, err := download.PgpBytesFromSource(v.log, check, v.client) if err != nil { return err } + if len(raw) == 0 { continue } diff --git a/internal/pkg/agent/application/upgrade/artifact/download/verifier.go b/internal/pkg/agent/application/upgrade/artifact/download/verifier.go index 36e0197a0f1..2c716cb2844 100644 --- a/internal/pkg/agent/application/upgrade/artifact/download/verifier.go +++ b/internal/pkg/agent/application/upgrade/artifact/download/verifier.go @@ -20,10 +20,11 @@ import ( "strings" "time" - "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/artifact" + "github.com/hashicorp/go-multierror" "golang.org/x/crypto/openpgp" //nolint:staticcheck // crypto/openpgp is only receiving security updates. + "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/artifact" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" ) @@ -32,6 +33,17 @@ const ( PgpSourceURIPrefix = "pgp_uri:" ) +var ( + ErrRemotePGPDownloadFailed = errors.New("Remote PGP download failed") + ErrInvalidLocation = errors.New("Remote PGP location is invalid") +) + +// warnLogger is a logger that only needs to implement Warnf, as that is the only functions +// that the downloadProgressReporter uses. +type warnLogger interface { + Warnf(format string, args ...interface{}) +} + // ChecksumMismatchError indicates the expected checksum for a file does not // match the computed checksum. type ChecksumMismatchError struct { @@ -168,13 +180,17 @@ func VerifyGPGSignature(file string, asciiArmorSignature, publicKey []byte) erro return nil } -func PgpBytesFromSource(source string, client http.Client) ([]byte, error) { +func PgpBytesFromSource(log warnLogger, source string, client http.Client) ([]byte, error) { if strings.HasPrefix(source, PgpSourceRawPrefix) { return []byte(strings.TrimPrefix(source, PgpSourceRawPrefix)), nil } if strings.HasPrefix(source, PgpSourceURIPrefix) { - return fetchPgpFromURI(strings.TrimPrefix(source, PgpSourceURIPrefix), client) + pgpBytes, err := fetchPgpFromURI(strings.TrimPrefix(source, PgpSourceURIPrefix), client) + if errors.Is(err, ErrRemotePGPDownloadFailed) || errors.Is(err, ErrInvalidLocation) { + log.Warnf("Skipped remote PGP located at %q because it's unavailable: %v", strings.TrimPrefix(source, PgpSourceURIPrefix), err) + } + return pgpBytes, nil } return nil, errors.New("unknown pgp source") @@ -187,7 +203,7 @@ func CheckValidDownloadUri(rawURI string) error { } if !strings.EqualFold(uri.Scheme, "https") { - return fmt.Errorf("failed to check URI %q: HTTPS is required", rawURI) + return multierror.Append(fmt.Errorf("failed to check URI %q: HTTPS is required", rawURI), ErrInvalidLocation) } return nil @@ -207,7 +223,7 @@ func fetchPgpFromURI(uri string, client http.Client) ([]byte, error) { } resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, multierror.Append(err, ErrRemotePGPDownloadFailed) } defer resp.Body.Close() diff --git a/magefile.go b/magefile.go index 7a37947d21d..3d0e6de1e9e 100644 --- a/magefile.go +++ b/magefile.go @@ -1728,10 +1728,13 @@ func createTestRunner(matrix bool, singleTest string, goTestFlags string, batche if datacenter == "" { datacenter = "us-central1-a" } + + // Valid values are gcp-us-central1 (default), azure-eastus2 essRegion := os.Getenv("TEST_INTEG_AUTH_ESS_REGION") if essRegion == "" { essRegion = "gcp-us-central1" } + instanceProvisionerMode := os.Getenv("INSTANCE_PROVISIONER") if instanceProvisionerMode == "" { instanceProvisionerMode = "ogc" diff --git a/pkg/testing/ess/deployment.go b/pkg/testing/ess/deployment.go index 5fe20eca5ee..9d9469036b0 100644 --- a/pkg/testing/ess/deployment.go +++ b/pkg/testing/ess/deployment.go @@ -12,6 +12,7 @@ import ( "html/template" "net/http" "net/url" + "strings" "time" ) @@ -84,9 +85,9 @@ type DeploymentStatusResponse struct { // CreateDeployment creates the deployment with the specified configuration. func (c *Client) CreateDeployment(ctx context.Context, req CreateDeploymentRequest) (*CreateDeploymentResponse, error) { - tpl, err := template.New("create_deployment_request").Parse(createDeploymentRequestTemplate) + tpl, err := deploymentTemplateFactory(req) if err != nil { - return nil, fmt.Errorf("unable to parse deployment creation template: %w", err) + return nil, err } var buf bytes.Buffer @@ -307,8 +308,32 @@ func overallStatus(statuses ...DeploymentStatus) DeploymentStatus { return overallStatus } -// TODO: make work for cloud other than GCP -const createDeploymentRequestTemplate = ` +func deploymentTemplateFactory(req CreateDeploymentRequest) (*template.Template, error) { + regionParts := strings.Split(req.Region, "-") + if len(regionParts) < 2 { + return nil, fmt.Errorf("unable to parse CSP out of region [%s]", req.Region) + } + + csp := regionParts[0] + var tplStr string + switch csp { + case "gcp": + tplStr = createDeploymentRequestTemplateGCP + case "azure": + tplStr = createDeploymentRequestTemplateAzure + default: + return nil, fmt.Errorf("unsupported CSP [%s]", csp) + } + + tpl, err := template.New("create_deployment_request").Parse(tplStr) + if err != nil { + return nil, fmt.Errorf("unable to parse deployment creation template: %w", err) + } + + return tpl, nil +} + +const createDeploymentRequestTemplateGCP = ` { "resources": { "integrations_server": [ @@ -410,3 +435,106 @@ const createDeploymentRequestTemplate = ` "system_owned": false } }` + +const createDeploymentRequestTemplateAzure = ` +{ + "resources": { + "integrations_server": [ + { + "elasticsearch_cluster_ref_id": "main-elasticsearch", + "region": "{{ .Region }}", + "plan": { + "cluster_topology": [ + { + "instance_configuration_id": "azure.integrationsserver.fsv2.2", + "zone_count": 1, + "size": { + "resource": "memory", + "value": 1024 + } + } + ], + "integrations_server": { + "version": "{{ .Version }}" + } + }, + "ref_id": "main-integrations_server" + } + ], + "elasticsearch": [ + { + "region": "{{ .Region }}", + "settings": { + "dedicated_masters_threshold": 6 + }, + "plan": { + "cluster_topology": [ + { + "zone_count": 1, + "elasticsearch": { + "node_attributes": { + "data": "hot" + } + }, + "instance_configuration_id": "azure.es.datahot.edsv4", + "node_roles": [ + "master", + "ingest", + "transform", + "data_hot", + "remote_cluster_client", + "data_content" + ], + "id": "hot_content", + "size": { + "resource": "memory", + "value": 8192 + } + } + ], + "elasticsearch": { + "version": "{{ .Version }}", + "enabled_built_in_plugins": [] + }, + "deployment_template": { + "id": "azure-storage-optimized-v2" + } + }, + "ref_id": "main-elasticsearch" + } + ], + "enterprise_search": [], + "kibana": [ + { + "elasticsearch_cluster_ref_id": "main-elasticsearch", + "region": "{{ .Region }}", + "plan": { + "cluster_topology": [ + { + "instance_configuration_id": "azure.kibana.fsv2", + "zone_count": 1, + "size": { + "resource": "memory", + "value": 1024 + } + } + ], + "kibana": { + "version": "{{ .Version }}", + "user_settings_json": { + "xpack.fleet.enableExperimental": ["agentTamperProtectionEnabled"] + } + } + }, + "ref_id": "main-kibana" + } + ] + }, + "settings": { + "autoscaling_enabled": false + }, + "name": "{{ .Name }}", + "metadata": { + "system_owned": false + } +}` diff --git a/testing/integration/upgrade_test.go b/testing/integration/upgrade_test.go index 8bf392c7f03..79c6f3658c8 100644 --- a/testing/integration/upgrade_test.go +++ b/testing/integration/upgrade_test.go @@ -222,7 +222,7 @@ func TestStandaloneUpgrade(t *testing.T) { parsedUpgradeVersion, err := version.ParseVersion(define.Version()) require.NoErrorf(t, err, "define.Version() %q cannot be parsed as agent version", define.Version()) skipVerify := version_8_7_0.Less(*parsedVersion) - testStandaloneUpgrade(ctx, t, agentFixture, parsedVersion, parsedUpgradeVersion, "", skipVerify, true, false, "") + testStandaloneUpgrade(ctx, t, agentFixture, parsedVersion, parsedUpgradeVersion, "", skipVerify, true, false, CustomPGP{}) }) } } @@ -268,13 +268,73 @@ func TestStandaloneUpgradeWithGPGFallback(t *testing.T) { _, defaultPGP := release.PGP() firstSeven := string(defaultPGP[:7]) - customPGP := strings.Replace( + newPgp := strings.Replace( string(defaultPGP), firstSeven, "abcDEFg", 1, ) + customPGP := CustomPGP{ + PGP: newPgp, + } + + testStandaloneUpgrade(ctx, t, agentFixture, fromVersion, toVersion, "", false, false, true, customPGP) +} + +func TestStandaloneUpgradeWithGPGFallbackOneRemoteFailing(t *testing.T) { + define.Require(t, define.Requirements{ + Local: false, // requires Agent installation + Sudo: true, // requires Agent installation + }) + + t.Skip("Fails upgrading to a version that doesn't exist: https://github.com/elastic/elastic-agent/issues/3397") + + minVersion := version_8_10_0_SNAPSHOT + fromVersion, err := version.ParseVersion(define.Version()) + require.NoError(t, err) + + if fromVersion.Less(*minVersion) { + t.Skipf("Version %s is lower than min version %s", define.Version(), minVersion) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // previous + toVersion, err := fromVersion.GetPreviousMinor() + require.NoError(t, err, "failed to get previous minor") + agentFixture, err := define.NewFixture( + t, + define.Version(), + ) + require.NoError(t, err, "error creating fixture") + + err = agentFixture.Prepare(ctx) + require.NoError(t, err, "error preparing agent fixture") + + err = agentFixture.Configure(ctx, []byte(fastWatcherCfg)) + require.NoError(t, err, "error configuring agent fixture") + + t.Cleanup(func() { + // The watcher needs to finish before the agent is uninstalled: https://github.com/elastic/elastic-agent/issues/3371 + waitForUpgradeWatcherToComplete(t, agentFixture, fromVersion, standaloneWatcherDuration) + }) + + _, defaultPGP := release.PGP() + firstSeven := string(defaultPGP[:7]) + newPgp := strings.Replace( + string(defaultPGP), + firstSeven, + "abcDEFg", + 1, + ) + + customPGP := CustomPGP{ + PGP: newPgp, + PGPUri: "https://127.0.0.1:3456/non/existing/path", + } + testStandaloneUpgrade(ctx, t, agentFixture, fromVersion, toVersion, "", false, false, true, customPGP) } @@ -356,7 +416,7 @@ func TestStandaloneUpgradeToSpecificSnapshotBuild(t *testing.T) { }) require.NoErrorf(t, err, "define.Version() %q cannot be parsed as agent version", define.Version()) - testStandaloneUpgrade(ctx, t, agentFixture, parsedFromVersion, upgradeInputVersion, expectedAgentHashAfterUpgrade, false, true, false, "") + testStandaloneUpgrade(ctx, t, agentFixture, parsedFromVersion, upgradeInputVersion, expectedAgentHashAfterUpgrade, false, true, false, CustomPGP{}) } func getUpgradableVersions(ctx context.Context, t *testing.T, upgradeToVersion string) (upgradableVersions []*version.ParsedSemVer) { @@ -431,7 +491,7 @@ func testStandaloneUpgrade( allowLocalPackage bool, skipVerify bool, skipDefaultPgp bool, - customPgp string, + customPgp CustomPGP, ) { var nonInteractiveFlag bool @@ -483,8 +543,16 @@ func testStandaloneUpgrade( upgradeCmdArgs = append(upgradeCmdArgs, "--skip-default-pgp") } - if len(customPgp) > 0 { - upgradeCmdArgs = append(upgradeCmdArgs, "--pgp", customPgp) + if len(customPgp.PGP) > 0 { + upgradeCmdArgs = append(upgradeCmdArgs, "--pgp", customPgp.PGP) + } + + if len(customPgp.PGPUri) > 0 { + upgradeCmdArgs = append(upgradeCmdArgs, "--pgp-uri", customPgp.PGPUri) + } + + if len(customPgp.PGPPath) > 0 { + upgradeCmdArgs = append(upgradeCmdArgs, "--pgp-path", customPgp.PGPPath) } upgradeTriggerOutput, err := f.Exec(ctx, upgradeCmdArgs) @@ -923,3 +991,106 @@ func removePackageVersionFiles(t *testing.T, f *atesting.Fixture) { require.NoErrorf(t, err, "error removing package version file %q", vFile) } } + +// TestStandaloneUpgradeFailsStatus tests the scenario where upgrading to a new version +// of Agent fails due to the new Agent binary reporting an unhealthy status. It checks +// that the Agent is rolled back to the previous version. +func TestStandaloneUpgradeFailsStatus(t *testing.T) { + define.Require(t, define.Requirements{ + Local: false, // requires Agent installation + Isolate: false, + Sudo: true, // requires Agent installation + }) + + t.Skip("Affected by https://github.com/elastic/elastic-agent/issues/3371, watcher left running at end of test") + + upgradeFromVersion, err := version.ParseVersion(define.Version()) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Get available versions from Artifacts API + aac := tools.NewArtifactAPIClient() + versionList, err := aac.GetVersions(ctx) + require.NoError(t, err) + require.NotEmpty(t, versionList.Versions, "Artifact API returned no versions") + + // Determine the version that's TWO versions behind the latest. This is necessary for two reasons: + // 1. We don't want to necessarily use the latest version as it might be the same as the + // local one, which will then cause the invalid input in the Agent test policy (defined further + // below in this test) to come into play with the Agent version we're upgrading from, thus preventing + // it from ever becoming healthy. + // 2. We don't want to necessarily use the version that's one before the latest because sometimes we + // are in a situation where the latest version has been advanced to the next release (e.g. 8.10.0) + // but the version before that (e.g. 8.9.0) hasn't been released yet. + require.GreaterOrEqual(t, len(versionList.Versions), 3) + upgradeToVersionStr := versionList.Versions[len(versionList.Versions)-3] + + upgradeToVersion, err := version.ParseVersion(upgradeToVersionStr) + require.NoError(t, err) + + t.Logf("Testing Elastic Agent upgrade from %s to %s...", upgradeFromVersion, upgradeToVersion) + + agentFixture, err := define.NewFixture(t, define.Version()) + require.NoError(t, err) + + err = agentFixture.Prepare(ctx) + require.NoError(t, err, "error preparing agent fixture") + + // Configure Agent with fast watcher configuration and also an invalid + // input when the Agent version matches the upgraded Agent version. This way + // the pre-upgrade version of the Agent runs healthy, but the post-upgrade + // version doesn't. + invalidInputPolicy := fastWatcherCfg + fmt.Sprintf(` +outputs: + default: + type: elasticsearch + hosts: [127.0.0.1:9200] + +inputs: + - condition: '${agent.version.version} == "%s"' + type: invalid + id: invalid-input +`, upgradeToVersion.CoreVersion()) + + err = agentFixture.Configure(ctx, []byte(invalidInputPolicy)) + require.NoError(t, err, "error configuring agent fixture") + + t.Log("Install the built Agent") + output, err := tools.InstallStandaloneAgent(agentFixture) + t.Log(string(output)) + require.NoError(t, err) + + c := agentFixture.Client() + require.Eventually(t, func() bool { + return checkAgentHealthAndVersion(t, ctx, agentFixture, upgradeFromVersion.CoreVersion(), upgradeFromVersion.IsSnapshot(), "") + }, 2*time.Minute, 10*time.Second, "Agent never became healthy") + + toVersion := upgradeToVersion.String() + t.Logf("Upgrading Agent to %s", toVersion) + err = c.Connect(ctx) + require.NoError(t, err, "error connecting client to agent") + defer c.Disconnect() + + _, err = c.Upgrade(ctx, toVersion, "", false, false) + require.NoErrorf(t, err, "error triggering agent upgrade to version %q", toVersion) + + require.Eventually(t, func() bool { + return checkAgentHealthAndVersion(t, ctx, agentFixture, upgradeToVersion.CoreVersion(), upgradeToVersion.IsSnapshot(), "") + }, 2*time.Minute, 250*time.Millisecond, "Upgraded Agent never became healthy") + + // Wait for upgrade watcher to finish running + waitForUpgradeWatcherToComplete(t, agentFixture, upgradeFromVersion, standaloneWatcherDuration) + + t.Log("Ensure the we have rolled back and the correct version is running") + require.Eventually(t, func() bool { + return checkAgentHealthAndVersion(t, ctx, agentFixture, upgradeFromVersion.CoreVersion(), upgradeFromVersion.IsSnapshot(), "") + }, 2*time.Minute, 10*time.Second, "Rolled back Agent never became healthy") +} + +type CustomPGP struct { + PGP string + PGPUri string + PGPPath string +} diff --git a/version/docs/version.asciidoc b/version/docs/version.asciidoc index d5ed83978f5..d1d21059023 100644 --- a/version/docs/version.asciidoc +++ b/version/docs/version.asciidoc @@ -1,4 +1,4 @@ -:stack-version: 8.10.1 +:stack-version: 8.10.2 :doc-branch: 8.10 // FIXME: once elastic.co docs have been switched over to use `main`, remove // the `doc-site-branch` line below as well as any references to it in the code.