Skip to content

Commit ac9ee9a

Browse files
authored
[Integration Test] Ensure that upgrading a FIPS-capable Agent results in a FIPS-capable Agent (#7804)
* Adding skeleton for FIPS-to-FIPS upgrade test * Expose FIPS compliance in GRPC client Version call response * Test upgrade from FIPS to FIPS artifact * Change assert to require * Add postWatcherSuccessHook to upgrade test * Refactor standalone upgrade test to take upgradeOpts * Fix up FIPS upgrade test to use postWatcherSuccessHook to test FIPS compliance of upgraded Agent * Add version constraint to test * s/compliant/capable/ * s/compliant/capable/ * Append -fips to artifact name if current release of Agent is FIPS-capable * Enable FIPS-capable to FIPS-capable Agent upgrades * Running mage fmt * Adding test to ensure FIPS-capable Agent cannot be upgraded to non-FIPS-capable Agent * Adding return value * Fixing function calls * Remove post-upgrade success hook since we expect upgrade to fail * Add minimum FIPS version check for start version * Simplify upgradeOpts initialization * Add version equality comparison method * Fix version checks in tests * Refactor version check into own helper function * Fixing args * No need to pass testing.T * Remove redundant test case * Restrict FIPS integration tests to Linux * Add Fleet-managed Agent FIPS upgrade test * Remove integration test trying to upgrade FIPS to non-FIPS * Fix type of argument * Refactoring: extract common logic into helper function * Remove old code * Require no error * Fixing syntax errors * Define tests as needing a FIPS environment * FIPS upgrade tests should only run on Linux * FIPS upgrade tests should start with FIPS-capable version * Fixing comment + skip message * Fix syntax errors * Removing TestStandaloneUpgradeFIPStoFIPS test * Removing TestFleetManagedUpgradePrivilegedFIPS test * Add back accidentally-deleted function * Combine less and equal unit tests * Hash replaceToken only if its non-empty * Use startFixture
1 parent 863a890 commit ac9ee9a

File tree

17 files changed

+354
-64
lines changed

17 files changed

+354
-64
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package artifact
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
agtversion "github.com/elastic/elastic-agent/pkg/version"
13+
)
14+
15+
func TestGetArtifactName(t *testing.T) {
16+
version, err := agtversion.ParseVersion("9.1.0")
17+
require.NoError(t, err)
18+
19+
tests := map[string]struct {
20+
a Artifact
21+
version agtversion.ParsedSemVer
22+
operatingSystem string
23+
arch string
24+
expectedName string
25+
expectedErr string
26+
}{
27+
"no_fips": {
28+
a: Artifact{Cmd: "elastic-agent"},
29+
version: *version,
30+
operatingSystem: "linux",
31+
arch: "arm64",
32+
expectedName: "elastic-agent-9.1.0-linux-arm64.tar.gz",
33+
},
34+
"fips": {
35+
a: Artifact{Cmd: "elastic-agent-fips"},
36+
version: *version,
37+
operatingSystem: "linux",
38+
arch: "arm64",
39+
expectedName: "elastic-agent-fips-9.1.0-linux-arm64.tar.gz",
40+
},
41+
}
42+
43+
for name, test := range tests {
44+
t.Run(name, func(t *testing.T) {
45+
artifactName, err := GetArtifactName(test.a, test.version, test.operatingSystem, test.arch)
46+
if test.expectedErr == "" {
47+
require.NoError(t, err)
48+
require.Equal(t, test.expectedName, artifactName)
49+
} else {
50+
require.Empty(t, artifactName)
51+
require.Equal(t, test.expectedErr, err.Error())
52+
}
53+
})
54+
}
55+
56+
}

internal/pkg/agent/application/upgrade/step_unpack.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,10 @@ func readCommitHash(reader io.Reader) (string, error) {
599599
}
600600

601601
func getFileNamePrefix(archivePath string) string {
602-
return strings.TrimSuffix(filepath.Base(archivePath), ".tar.gz") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename
602+
prefix := strings.TrimSuffix(filepath.Base(archivePath), ".tar.gz") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename
603+
prefix = strings.Replace(prefix, fipsPrefix, "", 1)
604+
605+
return prefix
603606
}
604607

605608
func validFileName(p string) bool {

internal/pkg/agent/application/upgrade/step_unpack_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,3 +625,27 @@ func addEntryToZipArchive(af files, writer *zip.Writer) error {
625625

626626
return nil
627627
}
628+
629+
func TestGetFileNamePrefix(t *testing.T) {
630+
tests := map[string]struct {
631+
archivePath string
632+
expectedPrefix string
633+
}{
634+
"fips": {
635+
archivePath: "/foo/bar/elastic-agent-fips-9.1.0-SNAPSHOT-linux-arm64.tar.gz",
636+
expectedPrefix: "elastic-agent-9.1.0-SNAPSHOT-linux-arm64/",
637+
},
638+
"no_fips": {
639+
archivePath: "/foo/bar/elastic-agent-9.1.0-SNAPSHOT-linux-arm64.tar.gz",
640+
expectedPrefix: "elastic-agent-9.1.0-SNAPSHOT-linux-arm64/",
641+
},
642+
}
643+
644+
for name, test := range tests {
645+
t.Run(name, func(t *testing.T) {
646+
prefix := getFileNamePrefix(test.archivePath)
647+
require.Equal(t, test.expectedPrefix, prefix)
648+
})
649+
}
650+
651+
}

internal/pkg/agent/application/upgrade/upgrade.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const (
4646
runDirMod = 0770
4747
snapshotSuffix = "-SNAPSHOT"
4848
watcherMaxWaitTime = 30 * time.Second
49+
fipsPrefix = "-fips"
4950
)
5051

5152
var agentArtifact = artifact.Artifact{
@@ -61,6 +62,12 @@ var (
6162
ErrFipsToNonFips = errors.New("cannot switch to non-fips mode when upgrading")
6263
)
6364

65+
func init() {
66+
if release.FIPSDistribution() {
67+
agentArtifact.Cmd += fipsPrefix
68+
}
69+
}
70+
6471
// Upgrader performs an upgrade
6572
type Upgrader struct {
6673
log *logger.Logger
@@ -174,10 +181,12 @@ func checkUpgrade(log *logger.Logger, currentVersion, newVersion agentVersion, m
174181
}
175182

176183
if currentVersion.fips && !metadata.manifest.Package.Fips {
184+
log.Warnf("Upgrade action skipped because FIPS-capable Agent cannot be upgraded to non-FIPS-capable Agent")
177185
return ErrFipsToNonFips
178186
}
179187

180188
if !currentVersion.fips && metadata.manifest.Package.Fips {
189+
log.Warnf("Upgrade action skipped because non-FIPS-capable Agent cannot be upgraded to FIPS-capable Agent")
181190
return ErrNonFipsToFips
182191
}
183192

internal/pkg/agent/cmd/enroll_cmd.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,10 +1051,18 @@ func createFleetConfigFromEnroll(accessAPIKey string, enrollmentToken string, re
10511051
if err != nil {
10521052
return nil, errors.New(err, "failed to generate enrollment hash", errors.TypeConfig)
10531053
}
1054-
cfg.ReplaceTokenHash, err = fleetHashToken(replaceToken)
1055-
if err != nil {
1056-
return nil, errors.New(err, "failed to generate replace token hash", errors.TypeConfig)
1054+
1055+
// Hash replaceToken if provided; it is not expected to be provided when an Agent
1056+
// is being enrolled for the very first time. Hashing an empty replaceToken with the
1057+
// FIPS-capable build of Elastic Agent results in an "invalid key length" error from
1058+
// OpenSSL's FIPS provider.
1059+
if replaceToken != "" {
1060+
cfg.ReplaceTokenHash, err = fleetHashToken(replaceToken)
1061+
if err != nil {
1062+
return nil, errors.New(err, "failed to generate replace token hash", errors.TypeConfig)
1063+
}
10571064
}
1065+
10581066
if err := cfg.Valid(); err != nil {
10591067
return nil, errors.New(err, "invalid enrollment options", errors.TypeConfig)
10601068
}

pkg/control/v2/client/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ func (c *client) Version(ctx context.Context) (Version, error) {
300300
Commit: res.Commit,
301301
BuildTime: bt,
302302
Snapshot: res.Snapshot,
303+
Fips: res.Fips,
303304
}, nil
304305
}
305306

pkg/testing/define/define.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"sync"
1919
"testing"
2020

21+
"github.com/stretchr/testify/require"
22+
2123
"github.com/gofrs/uuid/v5"
2224

2325
"github.com/elastic/elastic-agent-libs/kibana"
@@ -83,33 +85,31 @@ func Version() string {
8385
// NewFixtureFromLocalBuild returns a new Elastic Agent testing fixture with a LocalFetcher and
8486
// the agent logging to the test logger.
8587
func NewFixtureFromLocalBuild(t *testing.T, version string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
86-
buildsDir := os.Getenv("AGENT_BUILD_DIR")
87-
if buildsDir == "" {
88-
projectDir, err := findProjectRoot()
89-
if err != nil {
90-
return nil, err
91-
}
92-
buildsDir = filepath.Join(projectDir, "build", "distributions")
93-
}
94-
95-
return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir, opts...)
88+
return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir(t), false, opts...)
89+
}
9690

91+
// NewFixtureFromLocalFIPSBuild returns a new FIPS-capable Elastic Agent testing fixture with a LocalFetcher
92+
// and the agent logging to the test logger.
93+
func NewFixtureFromLocalFIPSBuild(t *testing.T, version string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
94+
return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir(t), true, opts...)
9795
}
9896

9997
// NewFixtureWithBinary returns a new Elastic Agent testing fixture with a LocalFetcher and
10098
// the agent logging to the test logger.
101-
func NewFixtureWithBinary(t *testing.T, version string, binary string, buildsDir string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
99+
func NewFixtureWithBinary(t *testing.T, version string, binary string, buildsDir string, fips bool, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
102100
ver, err := semver.ParseVersion(version)
103101
if err != nil {
104102
return nil, fmt.Errorf("%q is an invalid agent version: %w", version, err)
105103
}
106104

107-
var binFetcher atesting.Fetcher
105+
localFetcherOpts := []atesting.LocalFetcherOpt{atesting.WithCustomBinaryName(binary)}
108106
if ver.IsSnapshot() {
109-
binFetcher = atesting.LocalFetcher(buildsDir, atesting.WithLocalSnapshotOnly(), atesting.WithCustomBinaryName(binary))
110-
} else {
111-
binFetcher = atesting.LocalFetcher(buildsDir, atesting.WithCustomBinaryName(binary))
107+
localFetcherOpts = append(localFetcherOpts, atesting.WithLocalSnapshotOnly())
112108
}
109+
if fips {
110+
localFetcherOpts = append(localFetcherOpts, atesting.WithLocalFIPSOnly())
111+
}
112+
binFetcher := atesting.LocalFetcher(buildsDir, localFetcherOpts...)
113113

114114
opts = append(opts, atesting.WithFetcher(binFetcher), atesting.WithLogOutput())
115115
if binary != "elastic-agent" {
@@ -301,3 +301,16 @@ func getKibanaClient() (*kibana.Client, error) {
301301
}
302302
return c, nil
303303
}
304+
305+
func buildsDir(t *testing.T) string {
306+
t.Helper()
307+
308+
buildsDir := os.Getenv("AGENT_BUILD_DIR")
309+
if buildsDir == "" {
310+
projectDir, err := findProjectRoot()
311+
require.NoError(t, err)
312+
buildsDir = filepath.Join(projectDir, "build", "distributions")
313+
}
314+
315+
return buildsDir
316+
}

pkg/testing/fetcher_local.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,35 @@ import (
1818
type localFetcher struct {
1919
dir string
2020
snapshotOnly bool
21+
fipsOnly bool
2122
binaryName string
2223
}
2324

24-
type localFetcherOpt func(f *localFetcher)
25+
type LocalFetcherOpt func(f *localFetcher)
2526

2627
// WithLocalSnapshotOnly sets the LocalFetcher to only pull the snapshot build.
27-
func WithLocalSnapshotOnly() localFetcherOpt {
28+
func WithLocalSnapshotOnly() LocalFetcherOpt {
2829
return func(f *localFetcher) {
2930
f.snapshotOnly = true
3031
}
3132
}
3233

34+
// WithLocalFIPSOnly sets the LocalFetcher to only pull a FIPS-compliant build.
35+
func WithLocalFIPSOnly() LocalFetcherOpt {
36+
return func(f *localFetcher) {
37+
f.fipsOnly = true
38+
}
39+
}
40+
3341
// WithCustomBinaryName sets the binary to a custom name, the default is `elastic-agent`
34-
func WithCustomBinaryName(name string) localFetcherOpt {
42+
func WithCustomBinaryName(name string) LocalFetcherOpt {
3543
return func(f *localFetcher) {
3644
f.binaryName = name
3745
}
3846
}
3947

4048
// LocalFetcher returns a fetcher that pulls the binary of the Elastic Agent from a local location.
41-
func LocalFetcher(dir string, opts ...localFetcherOpt) Fetcher {
49+
func LocalFetcher(dir string, opts ...LocalFetcherOpt) Fetcher {
4250
f := &localFetcher{
4351
dir: dir,
4452
binaryName: "elastic-agent",
@@ -56,6 +64,7 @@ func (f *localFetcher) Name() string {
5664

5765
// Fetch fetches the Elastic Agent and places the resulting binary at the path.
5866
func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architecture string, version string, packageFormat string) (FetcherResult, error) {
67+
prefix := GetPackagePrefix(f.fipsOnly)
5968
suffix, err := GetPackageSuffix(operatingSystem, architecture, packageFormat)
6069
if err != nil {
6170
return nil, err
@@ -66,7 +75,7 @@ func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architec
6675
return nil, fmt.Errorf("invalid version: %q: %w", ver, err)
6776
}
6877

69-
mainBuildfmt := "%s-%s-%s"
78+
mainBuildfmt := "%s-%s%s-%s"
7079
if f.snapshotOnly && !ver.IsSnapshot() {
7180
if ver.Prerelease() == "" {
7281
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
8594
}
8695

8796
if ver.IsSnapshot() && !matchesEarlyReleaseVersion {
88-
build := fmt.Sprintf(mainBuildfmt, f.binaryName, ver.VersionWithPrerelease(), suffix)
97+
build := fmt.Sprintf(mainBuildfmt, f.binaryName, prefix, ver.VersionWithPrerelease(), suffix)
8998
buildPath = filepath.Join(ver.BuildMetadata(), build)
9099
} else {
91-
buildPath = fmt.Sprintf(mainBuildfmt, f.binaryName, ver.String(), suffix)
100+
buildPath = fmt.Sprintf(mainBuildfmt, f.binaryName, prefix, ver.String(), suffix)
92101
}
93102

94103
fullPath := filepath.Join(f.dir, buildPath)

pkg/testing/fetcher_local_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func TestLocalFetcher(t *testing.T) {
5757
tcs := []struct {
5858
name string
5959
version string
60-
opts []localFetcherOpt
60+
opts []LocalFetcherOpt
6161
want []byte
6262
wantHash []byte
6363
}{
@@ -69,7 +69,7 @@ func TestLocalFetcher(t *testing.T) {
6969
}, {
7070
name: "SnapshotOnly",
7171
version: baseVersion,
72-
opts: []localFetcherOpt{WithLocalSnapshotOnly()},
72+
opts: []LocalFetcherOpt{WithLocalSnapshotOnly()},
7373
want: snapshotContent,
7474
wantHash: snapshotContentHash,
7575
}, {
@@ -80,13 +80,13 @@ func TestLocalFetcher(t *testing.T) {
8080
}, {
8181
name: "version with snapshot and SnapshotOnly",
8282
version: baseVersion + "-SNAPSHOT",
83-
opts: []localFetcherOpt{WithLocalSnapshotOnly()},
83+
opts: []LocalFetcherOpt{WithLocalSnapshotOnly()},
8484
want: snapshotContent,
8585
wantHash: snapshotContentHash,
8686
}, {
8787
name: "version with snapshot and build ID",
8888
version: baseVersion + "-SNAPSHOT+l5snflwr",
89-
opts: []localFetcherOpt{},
89+
opts: []LocalFetcherOpt{},
9090
want: snapshotContent,
9191
wantHash: snapshotContentHash,
9292
},

pkg/testing/fixture.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type Fixture struct {
5050
binaryName string
5151
runLength time.Duration
5252
additionalArgs []string
53+
fipsArtifact bool
5354

5455
srcPackage string
5556
workDir string
@@ -145,6 +146,12 @@ func WithAdditionalArgs(args []string) FixtureOpt {
145146
}
146147
}
147148

149+
func WithFIPSArtifact() FixtureOpt {
150+
return func(f *Fixture) {
151+
f.fipsArtifact = true
152+
}
153+
}
154+
148155
// NewFixture creates a new fixture to setup and manage Elastic Agent.
149156
func NewFixture(t *testing.T, version string, opts ...FixtureOpt) (*Fixture, error) {
150157
// we store the caller so the fixture can find the cache directory for the artifacts that

0 commit comments

Comments
 (0)