diff --git a/internal/pkg/agent/application/upgrade/step_unpack.go b/internal/pkg/agent/application/upgrade/step_unpack.go index 90cc99c6934..34c2580542b 100644 --- a/internal/pkg/agent/application/upgrade/step_unpack.go +++ b/internal/pkg/agent/application/upgrade/step_unpack.go @@ -419,7 +419,10 @@ func readCommitHash(reader io.Reader) (string, error) { } func getFileNamePrefix(archivePath string) string { - return strings.TrimSuffix(filepath.Base(archivePath), ".tar.gz") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename + prefix := strings.TrimSuffix(filepath.Base(archivePath), ".tar.gz") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename + prefix = strings.Replace(prefix, fipsPrefix, "", 1) + + return prefix } func validFileName(p string) bool { diff --git a/internal/pkg/agent/application/upgrade/step_unpack_test.go b/internal/pkg/agent/application/upgrade/step_unpack_test.go index 295a5d3dbec..a7d1b1ae630 100644 --- a/internal/pkg/agent/application/upgrade/step_unpack_test.go +++ b/internal/pkg/agent/application/upgrade/step_unpack_test.go @@ -419,3 +419,27 @@ func addEntryToZipArchive(af files, writer *zip.Writer) error { return nil } + +func TestGetFileNamePrefix(t *testing.T) { + tests := map[string]struct { + archivePath string + expectedPrefix string + }{ + "fips": { + archivePath: "/foo/bar/elastic-agent-fips-9.1.0-SNAPSHOT-linux-arm64.tar.gz", + expectedPrefix: "elastic-agent-9.1.0-SNAPSHOT-linux-arm64/", + }, + "no_fips": { + archivePath: "/foo/bar/elastic-agent-9.1.0-SNAPSHOT-linux-arm64.tar.gz", + expectedPrefix: "elastic-agent-9.1.0-SNAPSHOT-linux-arm64/", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + prefix := getFileNamePrefix(test.archivePath) + require.Equal(t, test.expectedPrefix, prefix) + }) + } + +} diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index ba18bb6ff9c..04c91c3a7d0 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -46,6 +46,7 @@ const ( runDirMod = 0770 snapshotSuffix = "-SNAPSHOT" watcherMaxWaitTime = 30 * time.Second + fipsPrefix = "-fips" ) var agentArtifact = artifact.Artifact{ @@ -61,6 +62,12 @@ var ( ErrFipsToNonFips = errors.New("cannot switch to non-fips mode when upgrading") ) +func init() { + if release.FIPSDistribution() { + agentArtifact.Cmd += fipsPrefix + } +} + // Upgrader performs an upgrade type Upgrader struct { log *logger.Logger @@ -174,10 +181,12 @@ func checkUpgrade(log *logger.Logger, currentVersion, newVersion agentVersion, m } if currentVersion.fips && !metadata.manifest.Package.Fips { + log.Warnf("Upgrade action skipped because FIPS-capable Agent cannot be upgraded to non-FIPS-capable Agent") return ErrFipsToNonFips } if !currentVersion.fips && metadata.manifest.Package.Fips { + log.Warnf("Upgrade action skipped because non-FIPS-capable Agent cannot be upgraded to FIPS-capable Agent") return ErrNonFipsToFips } diff --git a/internal/pkg/agent/cmd/enroll_cmd.go b/internal/pkg/agent/cmd/enroll_cmd.go index 9f1129b8f39..99f06cb9a81 100644 --- a/internal/pkg/agent/cmd/enroll_cmd.go +++ b/internal/pkg/agent/cmd/enroll_cmd.go @@ -1054,10 +1054,18 @@ func createFleetConfigFromEnroll(accessAPIKey string, enrollmentToken string, re if err != nil { return nil, errors.New(err, "failed to generate enrollment hash", errors.TypeConfig) } - cfg.ReplaceTokenHash, err = fleetHashToken(replaceToken) - if err != nil { - return nil, errors.New(err, "failed to generate replace token hash", errors.TypeConfig) + + // Hash replaceToken if provided; it is not expected to be provided when an Agent + // is being enrolled for the very first time. Hashing an empty replaceToken with the + // FIPS-capable build of Elastic Agent results in an "invalid key length" error from + // OpenSSL's FIPS provider. + if replaceToken != "" { + cfg.ReplaceTokenHash, err = fleetHashToken(replaceToken) + if err != nil { + return nil, errors.New(err, "failed to generate replace token hash", errors.TypeConfig) + } } + if err := cfg.Valid(); err != nil { return nil, errors.New(err, "invalid enrollment options", errors.TypeConfig) } diff --git a/pkg/control/v2/client/client.go b/pkg/control/v2/client/client.go index 291f98bb799..f36f5c36c3c 100644 --- a/pkg/control/v2/client/client.go +++ b/pkg/control/v2/client/client.go @@ -300,6 +300,7 @@ func (c *client) Version(ctx context.Context) (Version, error) { Commit: res.Commit, BuildTime: bt, Snapshot: res.Snapshot, + Fips: res.Fips, }, nil } diff --git a/pkg/testing/define/define.go b/pkg/testing/define/define.go index 1f74c63c155..c5910d7aff9 100644 --- a/pkg/testing/define/define.go +++ b/pkg/testing/define/define.go @@ -18,6 +18,8 @@ import ( "sync" "testing" + "github.com/stretchr/testify/require" + "github.com/gofrs/uuid/v5" "github.com/elastic/elastic-agent-libs/kibana" @@ -90,33 +92,31 @@ func Version() string { // NewFixtureFromLocalBuild returns a new Elastic Agent testing fixture with a LocalFetcher and // the agent logging to the test logger. func NewFixtureFromLocalBuild(t *testing.T, version string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) { - buildsDir := os.Getenv("AGENT_BUILD_DIR") - if buildsDir == "" { - projectDir, err := findProjectRoot() - if err != nil { - return nil, err - } - buildsDir = filepath.Join(projectDir, "build", "distributions") - } - - return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir, opts...) + return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir(t), false, opts...) +} +// NewFixtureFromLocalFIPSBuild returns a new FIPS-capable Elastic Agent testing fixture with a LocalFetcher +// and the agent logging to the test logger. +func NewFixtureFromLocalFIPSBuild(t *testing.T, version string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) { + return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir(t), true, opts...) } // NewFixtureWithBinary returns a new Elastic Agent testing fixture with a LocalFetcher and // the agent logging to the test logger. -func NewFixtureWithBinary(t *testing.T, version string, binary string, buildsDir string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) { +func NewFixtureWithBinary(t *testing.T, version string, binary string, buildsDir string, fips bool, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) { ver, err := semver.ParseVersion(version) if err != nil { return nil, fmt.Errorf("%q is an invalid agent version: %w", version, err) } - var binFetcher atesting.Fetcher + localFetcherOpts := []atesting.LocalFetcherOpt{atesting.WithCustomBinaryName(binary)} if ver.IsSnapshot() { - binFetcher = atesting.LocalFetcher(buildsDir, atesting.WithLocalSnapshotOnly(), atesting.WithCustomBinaryName(binary)) - } else { - binFetcher = atesting.LocalFetcher(buildsDir, atesting.WithCustomBinaryName(binary)) + localFetcherOpts = append(localFetcherOpts, atesting.WithLocalSnapshotOnly()) } + if fips { + localFetcherOpts = append(localFetcherOpts, atesting.WithLocalFIPSOnly()) + } + binFetcher := atesting.LocalFetcher(buildsDir, localFetcherOpts...) opts = append(opts, atesting.WithFetcher(binFetcher), atesting.WithLogOutput()) if binary != "elastic-agent" { @@ -308,3 +308,16 @@ func getKibanaClient() (*kibana.Client, error) { } return c, nil } + +func buildsDir(t *testing.T) string { + t.Helper() + + buildsDir := os.Getenv("AGENT_BUILD_DIR") + if buildsDir == "" { + projectDir, err := findProjectRoot() + require.NoError(t, err) + buildsDir = filepath.Join(projectDir, "build", "distributions") + } + + return buildsDir +} diff --git a/pkg/testing/fetcher_local.go b/pkg/testing/fetcher_local.go index 8a1dc305675..ac92b77d1d5 100644 --- a/pkg/testing/fetcher_local.go +++ b/pkg/testing/fetcher_local.go @@ -18,27 +18,35 @@ import ( type localFetcher struct { dir string snapshotOnly bool + fipsOnly bool binaryName string } -type localFetcherOpt func(f *localFetcher) +type LocalFetcherOpt func(f *localFetcher) // WithLocalSnapshotOnly sets the LocalFetcher to only pull the snapshot build. -func WithLocalSnapshotOnly() localFetcherOpt { +func WithLocalSnapshotOnly() LocalFetcherOpt { return func(f *localFetcher) { f.snapshotOnly = true } } +// WithLocalFIPSOnly sets the LocalFetcher to only pull a FIPS-compliant build. +func WithLocalFIPSOnly() LocalFetcherOpt { + return func(f *localFetcher) { + f.fipsOnly = true + } +} + // WithCustomBinaryName sets the binary to a custom name, the default is `elastic-agent` -func WithCustomBinaryName(name string) localFetcherOpt { +func WithCustomBinaryName(name string) LocalFetcherOpt { return func(f *localFetcher) { f.binaryName = name } } // LocalFetcher returns a fetcher that pulls the binary of the Elastic Agent from a local location. -func LocalFetcher(dir string, opts ...localFetcherOpt) Fetcher { +func LocalFetcher(dir string, opts ...LocalFetcherOpt) Fetcher { f := &localFetcher{ dir: dir, binaryName: "elastic-agent", @@ -56,6 +64,7 @@ func (f *localFetcher) Name() string { // Fetch fetches the Elastic Agent and places the resulting binary at the path. func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architecture string, version string, packageFormat string) (FetcherResult, error) { + prefix := GetPackagePrefix(f.fipsOnly) suffix, err := GetPackageSuffix(operatingSystem, architecture, packageFormat) if err != nil { return nil, err @@ -66,7 +75,7 @@ func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architec return nil, fmt.Errorf("invalid version: %q: %w", ver, err) } - mainBuildfmt := "%s-%s-%s" + mainBuildfmt := "%s-%s%s-%s" if f.snapshotOnly && !ver.IsSnapshot() { if ver.Prerelease() == "" { ver = semver.NewParsedSemVer(ver.Major(), ver.Minor(), ver.Patch(), "SNAPSHOT", ver.BuildMetadata()) @@ -85,10 +94,10 @@ func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architec } if ver.IsSnapshot() && !matchesEarlyReleaseVersion { - build := fmt.Sprintf(mainBuildfmt, f.binaryName, ver.VersionWithPrerelease(), suffix) + build := fmt.Sprintf(mainBuildfmt, f.binaryName, prefix, ver.VersionWithPrerelease(), suffix) buildPath = filepath.Join(ver.BuildMetadata(), build) } else { - buildPath = fmt.Sprintf(mainBuildfmt, f.binaryName, ver.String(), suffix) + buildPath = fmt.Sprintf(mainBuildfmt, f.binaryName, prefix, ver.String(), suffix) } fullPath := filepath.Join(f.dir, buildPath) diff --git a/pkg/testing/fetcher_local_test.go b/pkg/testing/fetcher_local_test.go index 54d92593230..b05e48d303a 100644 --- a/pkg/testing/fetcher_local_test.go +++ b/pkg/testing/fetcher_local_test.go @@ -57,7 +57,7 @@ func TestLocalFetcher(t *testing.T) { tcs := []struct { name string version string - opts []localFetcherOpt + opts []LocalFetcherOpt want []byte wantHash []byte }{ @@ -69,7 +69,7 @@ func TestLocalFetcher(t *testing.T) { }, { name: "SnapshotOnly", version: baseVersion, - opts: []localFetcherOpt{WithLocalSnapshotOnly()}, + opts: []LocalFetcherOpt{WithLocalSnapshotOnly()}, want: snapshotContent, wantHash: snapshotContentHash, }, { @@ -80,13 +80,13 @@ func TestLocalFetcher(t *testing.T) { }, { name: "version with snapshot and SnapshotOnly", version: baseVersion + "-SNAPSHOT", - opts: []localFetcherOpt{WithLocalSnapshotOnly()}, + opts: []LocalFetcherOpt{WithLocalSnapshotOnly()}, want: snapshotContent, wantHash: snapshotContentHash, }, { name: "version with snapshot and build ID", version: baseVersion + "-SNAPSHOT+l5snflwr", - opts: []localFetcherOpt{}, + opts: []LocalFetcherOpt{}, want: snapshotContent, wantHash: snapshotContentHash, }, diff --git a/pkg/testing/fixture.go b/pkg/testing/fixture.go index b216ae1e0fb..a2251029690 100644 --- a/pkg/testing/fixture.go +++ b/pkg/testing/fixture.go @@ -50,6 +50,7 @@ type Fixture struct { binaryName string runLength time.Duration additionalArgs []string + fipsArtifact bool srcPackage string workDir string @@ -145,6 +146,12 @@ func WithAdditionalArgs(args []string) FixtureOpt { } } +func WithFIPSArtifact() FixtureOpt { + return func(f *Fixture) { + f.fipsArtifact = true + } +} + // NewFixture creates a new fixture to setup and manage Elastic Agent. func NewFixture(t *testing.T, version string, opts ...FixtureOpt) (*Fixture, error) { // we store the caller so the fixture can find the cache directory for the artifacts that diff --git a/pkg/version/version_parser.go b/pkg/version/version_parser.go index 8ecd95dcec0..70589525923 100644 --- a/pkg/version/version_parser.go +++ b/pkg/version/version_parser.go @@ -144,6 +144,10 @@ func (psv ParsedSemVer) IndependentBuildID() string { return "" } +func (psv ParsedSemVer) Equal(other ParsedSemVer) bool { + return !psv.Less(other) && !other.Less(psv) +} + func (psv ParsedSemVer) Less(other ParsedSemVer) bool { // compare major version if psv.major != other.major { diff --git a/pkg/version/version_parser_test.go b/pkg/version/version_parser_test.go index a023a772744..4eae7e794ca 100644 --- a/pkg/version/version_parser_test.go +++ b/pkg/version/version_parser_test.go @@ -435,56 +435,79 @@ func TestExtractSnapshotFromVersionString(t *testing.T) { } } -func TestLess(t *testing.T) { +func TestComparisons(t *testing.T) { testcases := []struct { name string leftVersion string rightVersion string - less bool + + less bool + equal bool }{ // major, minor, patch section + { + name: "major, minor, patch are all equal", + leftVersion: "7.17.10", + rightVersion: "7.17.10", + less: false, + equal: true, + }, { name: "major version less than ours", leftVersion: "7.17.10", rightVersion: "8.9.0", less: true, + equal: false, }, { name: "minor version less than ours", leftVersion: "8.6.2", rightVersion: "8.9.0", less: true, + equal: false, }, { name: "patch version less than ours", leftVersion: "8.7.0", rightVersion: "8.7.1", less: true, + equal: false, }, // prerelease section + { + name: "major, minor, patch, prerelease are all equal", + leftVersion: "8.9.0-SNAPSHOT", + rightVersion: "8.9.0-SNAPSHOT", + less: false, + equal: true, + }, { name: "prerelease is always less than non-prerelease", leftVersion: "8.9.0-SNAPSHOT", rightVersion: "8.9.0", less: true, + equal: false, }, { name: "2 prereleases are compared by their tokens", leftVersion: "8.9.0-SNAPSHOT", rightVersion: "8.9.0-er1", less: false, + equal: false, }, { name: "2 prereleases are compared by their tokens, reversed", leftVersion: "8.9.0-er1", rightVersion: "8.9.0-SNAPSHOT", less: true, + equal: false, }, { name: "2 prereleases have no specific order", leftVersion: "8.9.0-SNAPSHOT", rightVersion: "8.9.0-er1", less: false, + equal: false, }, // build metadata (these have no impact on precedence) { @@ -492,12 +515,14 @@ func TestLess(t *testing.T) { leftVersion: "8.9.0-SNAPSHOT+aaaaaa", rightVersion: "8.9.0-SNAPSHOT+bbbbbb", less: false, + equal: true, }, { name: "build metadata have no influence on precedence, reversed", leftVersion: "8.9.0-SNAPSHOT+bbbbbb", rightVersion: "8.9.0-SNAPSHOT+aaaaaa", less: false, + equal: true, }, // testcases taken from semver.org // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. @@ -506,68 +531,86 @@ func TestLess(t *testing.T) { leftVersion: "1.0.0-alpha", rightVersion: "1.0.0-alpha.1", less: true, + equal: false, }, { name: "numeric identifiers always have lower precedence than non-numeric identifiers", leftVersion: "1.0.0-alpha.1", rightVersion: "1.0.0-alpha.beta", less: true, + equal: false, }, { name: "minimum number of prerelease string tokens must be compared alphabetically", leftVersion: "1.0.0-alpha.beta", rightVersion: "1.0.0-beta", less: true, + equal: false, }, { name: "prerelease with fewer tokens is less than same prerelease with extra tokens #2", leftVersion: "1.0.0-beta", rightVersion: "1.0.0-beta.2", less: true, + equal: false, }, { name: "numeric identifiers must be compared numerically", leftVersion: "1.0.0-beta.2", rightVersion: "1.0.0-beta.11", less: true, + equal: false, }, { name: "string identifiers are compared lexically", leftVersion: "1.0.0-beta.11", rightVersion: "1.0.0-rc.1", less: true, + equal: false, }, { name: "prerelease versions have lower precedence than non-prerelease version ", leftVersion: "1.0.0-rc.1", rightVersion: "1.0.0", less: true, + equal: false, }, // independent section + { + name: "major, minor, patch, independent release is equal", + leftVersion: "8.9.0+build202405061022", + rightVersion: "8.9.0+build202405061022", + equal: true, + less: false, + }, { name: "independent release is always more than regular", leftVersion: "8.9.0+build202405061022", rightVersion: "8.9.0", less: false, + equal: false, }, { name: "prerelease is less than independent release", leftVersion: "8.9.0-SNAPSHOT", rightVersion: "8.9.0+build202405061022", less: true, + equal: false, }, { name: "older release is less", leftVersion: "8.9.0+build202305061022", rightVersion: "8.9.0+build202405061022", less: true, + equal: false, }, { name: "older release is less - reversed", leftVersion: "8.9.0+build202405061022", rightVersion: "8.9.0+build202305061022", less: false, + equal: false, }, } @@ -580,6 +623,7 @@ func TestLess(t *testing.T) { require.NoError(t, err) require.NotNil(t, right) assert.Equalf(t, tc.less, left.Less(*right), "Expected %s < %s = %v", tc.leftVersion, tc.rightVersion, tc.less) + assert.Equalf(t, tc.equal, left.Equal(*right), "Expected %s == %s = %v", tc.leftVersion, tc.rightVersion, tc.equal) }) } } diff --git a/testing/integration/beats_serverless_test.go b/testing/integration/beats_serverless_test.go index fba75a02d5d..edd00700351 100644 --- a/testing/integration/beats_serverless_test.go +++ b/testing/integration/beats_serverless_test.go @@ -78,7 +78,7 @@ func (runner *BeatRunner) SetupSuite() { } runner.T().Logf("running serverless tests with %s", runner.testbeatName) - agentFixture, err := define.NewFixtureWithBinary(runner.T(), define.Version(), runner.testbeatName, "/home/ubuntu", atesting.WithRunLength(time.Minute*3), atesting.WithAdditionalArgs([]string{"-E", "output.elasticsearch.allow_older_versions=true"})) + agentFixture, err := define.NewFixtureWithBinary(runner.T(), define.Version(), runner.testbeatName, "/home/ubuntu", false, atesting.WithRunLength(time.Minute*3), atesting.WithAdditionalArgs([]string{"-E", "output.elasticsearch.allow_older_versions=true"})) runner.agentFixture = agentFixture require.NoError(runner.T(), err) diff --git a/testing/integration/upgrade_fleet_test.go b/testing/integration/upgrade_fleet_test.go index dba70cf686b..b56a9e53e2d 100644 --- a/testing/integration/upgrade_fleet_test.go +++ b/testing/integration/upgrade_fleet_test.go @@ -51,7 +51,7 @@ func TestFleetManagedUpgradeUnprivileged(t *testing.T) { Local: false, // requires Agent installation Sudo: true, // requires Agent installation }) - testFleetManagedUpgrade(t, info, true) + testFleetManagedUpgrade(t, info, true, false) } // TestFleetManagedUpgradePrivileged tests that the build under test can retrieve an action from @@ -65,16 +65,57 @@ func TestFleetManagedUpgradePrivileged(t *testing.T) { Local: false, // requires Agent installation Sudo: true, // requires Agent installation }) - testFleetManagedUpgrade(t, info, false) + testFleetManagedUpgrade(t, info, false, false) } -func testFleetManagedUpgrade(t *testing.T, info *define.Info, unprivileged bool) { +// TestFleetManagedUpgradeUnprivilegedFIPS tests that the build under test can retrieve an action from +// Fleet and perform the upgrade as an unprivileged FIPS-capable Elastic Agent. It does not need to test +// all the combinations of versions as the standalone tests already perform those tests and +// would be redundant. +func TestFleetManagedUpgradeUnprivilegedFIPS(t *testing.T) { + info := define.Require(t, define.Requirements{ + Group: Fleet, + Stack: &define.Stack{}, + Local: false, // requires Agent installation + Sudo: true, // requires Agent installation + OS: []define.OS{ + {Type: define.Linux}, + }, + FIPS: true, + }) + + // parse the version we are testing + currentVersion, err := version.ParseVersion(define.Version()) + require.NoError(t, err) + + // We need to start the upgrade from a FIPS-capable version + if !isFIPSCapableVersion(currentVersion) { + t.Skipf( + "Minimum start version of FIPS-capable Agent for running this test is either %q or %q, current start version: %q", + *upgradetest.Version_8_19_0_SNAPSHOT, + *upgradetest.Version_9_1_0_SNAPSHOT, + currentVersion, + ) + } + + postWatcherSuccessHook := upgradetest.PostUpgradeAgentIsFIPSCapable + upgradeOpts := []upgradetest.UpgradeOpt{upgradetest.WithPostWatcherSuccessHook(postWatcherSuccessHook)} + testFleetManagedUpgrade(t, info, true, true, upgradeOpts...) +} + +func testFleetManagedUpgrade(t *testing.T, info *define.Info, unprivileged bool, fips bool, upgradeOpts ...upgradetest.UpgradeOpt) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() // Start at the build version as we want to test the retry // logic that is in the build. - startFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) + var startFixture *atesting.Fixture + var err error + if fips { + startFixture, err = define.NewFixtureFromLocalFIPSBuild(t, define.Version()) + } else { + startFixture, err = define.NewFixtureFromLocalBuild(t, define.Version()) + } require.NoError(t, err) err = startFixture.Prepare(ctx) require.NoError(t, err) @@ -105,7 +146,7 @@ func testFleetManagedUpgrade(t *testing.T, info *define.Info, unprivileged bool) t.Logf("Testing Elastic Agent upgrade from %s to %s with Fleet...", define.Version(), endVersionInfo.Binary.String()) - testUpgradeFleetManagedElasticAgent(ctx, t, info, startFixture, endFixture, defaultPolicy(), unprivileged) + testUpgradeFleetManagedElasticAgent(ctx, t, info, startFixture, endFixture, defaultPolicy(), unprivileged, upgradeOpts...) } func TestFleetAirGappedUpgradeUnprivileged(t *testing.T) { @@ -348,7 +389,15 @@ func testUpgradeFleetManagedElasticAgent( startFixture *atesting.Fixture, endFixture *atesting.Fixture, policy kibana.AgentPolicy, - unprivileged bool) { + unprivileged bool, + opts ...upgradetest.UpgradeOpt, +) { + + // use the passed in options to perform the upgrade + var upgradeOpts upgradetest.UpgradeOpts + for _, o := range opts { + o(&upgradeOpts) + } kibClient := info.KibanaClient @@ -470,6 +519,11 @@ func testUpgradeFleetManagedElasticAgent( // version, otherwise it's possible that it was rolled back to the original version err = upgradetest.CheckHealthyAndVersion(ctx, startFixture, endVersionInfo.Binary) assert.NoError(t, err) + + if upgradeOpts.PostWatcherSuccessHook != nil { + err = upgradeOpts.PostWatcherSuccessHook(ctx, startFixture) + require.NoError(t, err) + } } func defaultPolicy() kibana.AgentPolicy { @@ -658,3 +712,19 @@ func copyFile(t *testing.T, srcPath, dstPath string) { err = dst.Sync() require.NoError(t, err, "Failed to sync dst file") } + +func isFIPSCapableVersion(ver *version.ParsedSemVer) bool { + // Versions prior to 8.19.0-SNAPSHOT are not FIPS-capable + if ver.Less(*upgradetest.Version_8_19_0_SNAPSHOT) { + return false + } + + // The 9.0.x versions are not FIPS-capable + if ver.Major() == upgradetest.Version_9_0_0_SNAPSHOT.Major() && + ver.Minor() == upgradetest.Version_9_0_0_SNAPSHOT.Minor() { + return false + } + + // All versions starting with 9.1.0-SNAPSHOT are FIPS-capable + return true +} diff --git a/testing/integration/upgrade_standalone_test.go b/testing/integration/upgrade_standalone_test.go index f4c23b435a3..41e746d91d5 100644 --- a/testing/integration/upgrade_standalone_test.go +++ b/testing/integration/upgrade_standalone_test.go @@ -40,24 +40,24 @@ func TestStandaloneUpgrade(t *testing.T) { unprivilegedAvailable = true } t.Run(fmt.Sprintf("Upgrade %s to %s (privileged)", startVersion, define.Version()), func(t *testing.T) { - testStandaloneUpgrade(t, startVersion, define.Version(), false) + testStandaloneUpgrade(t, startVersion, define.Version(), atesting.ArtifactFetcher(), upgradetest.WithUnprivileged(false)) }) if unprivilegedAvailable { t.Run(fmt.Sprintf("Upgrade %s to %s (unprivileged)", startVersion, define.Version()), func(t *testing.T) { - testStandaloneUpgrade(t, startVersion, define.Version(), true) + testStandaloneUpgrade(t, startVersion, define.Version(), atesting.ArtifactFetcher(), upgradetest.WithUnprivileged(true)) }) } } } -func testStandaloneUpgrade(t *testing.T, startVersion *version.ParsedSemVer, endVersion string, unprivileged bool) { +func testStandaloneUpgrade(t *testing.T, startVersion *version.ParsedSemVer, endVersion string, fetcher atesting.Fetcher, upgradeOpts ...upgradetest.UpgradeOpt) { ctx, cancel := testcontext.WithDeadline(t, context.Background(), time.Now().Add(10*time.Minute)) defer cancel() startFixture, err := atesting.NewFixture( t, startVersion.String(), - atesting.WithFetcher(atesting.ArtifactFetcher()), + atesting.WithFetcher(fetcher), ) require.NoError(t, err, "error creating previous agent fixture") @@ -73,6 +73,6 @@ func testStandaloneUpgrade(t *testing.T, startVersion *version.ParsedSemVer, end return } - err = upgradetest.PerformUpgrade(ctx, startFixture, endFixture, t, upgradetest.WithUnprivileged(unprivileged)) + err = upgradetest.PerformUpgrade(ctx, startFixture, endFixture, t, upgradeOpts...) assert.NoError(t, err) } diff --git a/testing/upgradetest/upgrader.go b/testing/upgradetest/upgrader.go index 9f4d2c33283..605ad539e2b 100644 --- a/testing/upgradetest/upgrader.go +++ b/testing/upgradetest/upgrader.go @@ -34,7 +34,7 @@ type CustomPGP struct { PGPPath string } -type upgradeOpts struct { +type UpgradeOpts struct { sourceURI *string unprivileged *bool @@ -50,83 +50,92 @@ type upgradeOpts struct { // Disable check that enforces different hashed between the to and from version of upgrade disableHashCheck bool - preInstallHook func() error - postInstallHook func() error - preUpgradeHook func() error - postUpgradeHook func() error + preInstallHook func() error + postInstallHook func() error + preUpgradeHook func() error + postUpgradeHook func() error + PostWatcherSuccessHook func(context.Context, *atesting.Fixture) error } -type UpgradeOpt func(opts *upgradeOpts) +type UpgradeOpt func(opts *UpgradeOpts) // WithSourceURI sets a specific --source-uri for the upgrade // command. This doesn't change the verification of the upgrade // the resulting upgrade must still be the same agent provided // in the endFixture variable. func WithSourceURI(sourceURI string) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.sourceURI = &sourceURI } } // WithUnprivileged sets the install to be explicitly unprivileged. func WithUnprivileged(unprivileged bool) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.unprivileged = &unprivileged } } // WithSkipVerify sets the skip verify option for upgrade. func WithSkipVerify(skipVerify bool) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.skipVerify = skipVerify } } // WithSkipDefaultPgp sets the skip default pgp option for upgrade. func WithSkipDefaultPgp(skipDefaultPgp bool) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.skipDefaultPgp = skipDefaultPgp } } // WithCustomPGP sets a custom pgp configuration for upgrade. func WithCustomPGP(customPgp CustomPGP) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.customPgp = &customPgp } } // WithPreInstallHook sets a hook to be called before install. func WithPreInstallHook(hook func() error) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.preInstallHook = hook } } // WithPostInstallHook sets a hook to be called before install. func WithPostInstallHook(hook func() error) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.postInstallHook = hook } } // WithPreUpgradeHook sets a hook to be called before install. func WithPreUpgradeHook(hook func() error) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.preUpgradeHook = hook } } // WithPostUpgradeHook sets a hook to be called before install. func WithPostUpgradeHook(hook func() error) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.postUpgradeHook = hook } } +// WithPostWatcherSuccessHook sets a hook to be called after the upgrade is successful +// and the upgrade watcher has terminated as well. +func WithPostWatcherSuccessHook(hook func(context.Context, *atesting.Fixture) error) UpgradeOpt { + return func(opts *UpgradeOpts) { + opts.PostWatcherSuccessHook = hook + } +} + // WithCustomWatcherConfig sets a custom watcher configuration to use. func WithCustomWatcherConfig(cfg string) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.customWatcherCfg = cfg } } @@ -136,14 +145,14 @@ func WithCustomWatcherConfig(cfg string) UpgradeOpt { // useful in upgrade tests where the end Agent version does not contain changes // in the Upgrade Watcher whose effects are being asserted upon in PerformUpgrade. func WithDisableUpgradeWatcherUpgradeDetailsCheck() UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.disableUpgradeWatcherUpgradeDetailsCheck = true } } // WithDisableHashCheck disables hash check between start and end versions of upgrade func WithDisableHashCheck(disable bool) UpgradeOpt { - return func(opts *upgradeOpts) { + return func(opts *UpgradeOpts) { opts.disableHashCheck = disable } } @@ -159,7 +168,7 @@ func PerformUpgrade( // use the passed in options to perform the upgrade // `skipVerify` is by default enabled, because default is to perform a local // upgrade to a built version of the Elastic Agent. - var upgradeOpts upgradeOpts + var upgradeOpts UpgradeOpts upgradeOpts.skipVerify = true for _, o := range opts { o(&upgradeOpts) @@ -426,6 +435,12 @@ func PerformUpgrade( } } + if upgradeOpts.PostWatcherSuccessHook != nil { + if err := upgradeOpts.PostWatcherSuccessHook(ctx, endFixture); err != nil { + return fmt.Errorf("post watcher success hook failed: %w", err) + } + } + return nil } @@ -681,3 +696,24 @@ func getStatus(ctx context.Context, fixture *atesting.Fixture) *atesting.AgentSt } } } + +// PostUpgradeAgentIsFIPSCapable checks if the Agent fixture after upgrade is FIPS-capable and +// returns an error if it isn't. +func PostUpgradeAgentIsFIPSCapable(ctx context.Context, f *atesting.Fixture) error { + client := f.Client() + err := client.Connect(ctx) + if err != nil { + return err + } + + ver, err := client.Version(ctx) + if err != nil { + return err + } + + if !ver.Fips { + return errors.New("expected upgraded Agent to be FIPS-capable") + } + + return nil +} diff --git a/testing/upgradetest/versions.go b/testing/upgradetest/versions.go index 032d80ef880..cd42451e90a 100644 --- a/testing/upgradetest/versions.go +++ b/testing/upgradetest/versions.go @@ -40,6 +40,14 @@ var ( // Version_8_14_0_SNAPSHOT is the minimum version for proper unprivileged execution on all platforms Version_8_14_0_SNAPSHOT = version.NewParsedSemVer(8, 14, 0, "SNAPSHOT", "") + // Version 8_19_0_SNAPSHOT is a FIPS-capable artifact. + Version_8_19_0_SNAPSHOT = version.NewParsedSemVer(8, 19, 0, "SNAPSHOT", "") + + Version_9_0_0_SNAPSHOT = version.NewParsedSemVer(9, 0, 0, "SNAPSHOT", "") + + // Version_9_1_0_SNAPSHOT is a FIPS-capable artifact. + Version_9_1_0_SNAPSHOT = version.NewParsedSemVer(9, 1, 0, "SNAPSHOT", "") + // ErrNoSnapshot is returned when a requested snapshot is not on the version list. ErrNoSnapshot = errors.New("failed to find a snapshot on the version list") // ErrNoPreviousMinor is returned when a requested previous minor is not on the version list.