Skip to content

[Integration Test] Ensure that upgrading a FIPS-capable Agent results in a FIPS-capable Agent #7804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d85d426
Adding skeleton for FIPS-to-FIPS upgrade test
ycombinator Apr 7, 2025
1eb0710
Expose FIPS compliance in GRPC client Version call response
ycombinator Apr 9, 2025
ca8ca8d
Test upgrade from FIPS to FIPS artifact
ycombinator Apr 9, 2025
693e70c
Change assert to require
ycombinator Apr 11, 2025
0288739
Add postWatcherSuccessHook to upgrade test
ycombinator Apr 11, 2025
a554bdd
Refactor standalone upgrade test to take upgradeOpts
ycombinator Apr 11, 2025
bc75d8e
Fix up FIPS upgrade test to use postWatcherSuccessHook to test FIPS c…
ycombinator Apr 11, 2025
1ac77e5
Add version constraint to test
ycombinator Apr 14, 2025
6c0db36
s/compliant/capable/
ycombinator Apr 17, 2025
2ce34b0
s/compliant/capable/
ycombinator Apr 24, 2025
a3c2c67
Append -fips to artifact name if current release of Agent is FIPS-cap…
ycombinator Apr 25, 2025
cf2d751
Enable FIPS-capable to FIPS-capable Agent upgrades
ycombinator Apr 25, 2025
abd3cbd
Running mage fmt
ycombinator Apr 25, 2025
de2b163
Adding test to ensure FIPS-capable Agent cannot be upgraded to non-FI…
ycombinator Apr 25, 2025
e703db3
Adding return value
ycombinator Apr 26, 2025
66814c3
Fixing function calls
ycombinator Apr 26, 2025
197fc73
Remove post-upgrade success hook since we expect upgrade to fail
ycombinator Apr 26, 2025
0449544
Add minimum FIPS version check for start version
ycombinator Apr 26, 2025
fc0200e
Simplify upgradeOpts initialization
ycombinator Apr 26, 2025
e973530
Add version equality comparison method
ycombinator Apr 28, 2025
857e425
Fix version checks in tests
ycombinator Apr 28, 2025
5f43cbc
Refactor version check into own helper function
ycombinator Apr 28, 2025
c6e9138
Fixing args
ycombinator Apr 28, 2025
a668cc9
No need to pass testing.T
ycombinator Apr 28, 2025
772f025
Remove redundant test case
ycombinator Apr 29, 2025
8262637
Restrict FIPS integration tests to Linux
ycombinator May 2, 2025
28be696
Add Fleet-managed Agent FIPS upgrade test
ycombinator May 6, 2025
cea583a
Remove integration test trying to upgrade FIPS to non-FIPS
ycombinator May 6, 2025
4dc8306
Fix type of argument
ycombinator May 6, 2025
af39588
Refactoring: extract common logic into helper function
ycombinator May 6, 2025
b78720f
Remove old code
ycombinator May 6, 2025
eb8da3a
Require no error
ycombinator May 6, 2025
7d2161b
Fixing syntax errors
ycombinator May 6, 2025
f1162f9
Define tests as needing a FIPS environment
ycombinator May 7, 2025
34091e8
FIPS upgrade tests should only run on Linux
ycombinator May 7, 2025
0ec4dfb
FIPS upgrade tests should start with FIPS-capable version
ycombinator May 7, 2025
5a3fb65
Fixing comment + skip message
ycombinator May 7, 2025
142ec35
Fix syntax errors
ycombinator May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package artifact

import (
"testing"

"github.com/stretchr/testify/require"

agtversion "github.com/elastic/elastic-agent/pkg/version"
)

func TestGetArtifactName(t *testing.T) {
version, err := agtversion.ParseVersion("9.1.0")
require.NoError(t, err)

tests := map[string]struct {
a Artifact
version agtversion.ParsedSemVer
operatingSystem string
arch string
expectedName string
expectedErr string
}{
"no_fips": {
a: Artifact{Cmd: "elastic-agent"},
version: *version,
operatingSystem: "linux",
arch: "arm64",
expectedName: "elastic-agent-9.1.0-linux-arm64.tar.gz",
},
"fips": {
a: Artifact{Cmd: "elastic-agent-fips"},
version: *version,
operatingSystem: "linux",
arch: "arm64",
expectedName: "elastic-agent-fips-9.1.0-linux-arm64.tar.gz",
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
artifactName, err := GetArtifactName(test.a, test.version, test.operatingSystem, test.arch)
if test.expectedErr == "" {
require.NoError(t, err)
require.Equal(t, test.expectedName, artifactName)
} else {
require.Empty(t, artifactName)
require.Equal(t, test.expectedErr, err.Error())
}
})
}

}
5 changes: 4 additions & 1 deletion internal/pkg/agent/application/upgrade/step_unpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,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 {
Expand Down
24 changes: 24 additions & 0 deletions internal/pkg/agent/application/upgrade/step_unpack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,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)
})
}

}
9 changes: 9 additions & 0 deletions internal/pkg/agent/application/upgrade/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const (
runDirMod = 0770
snapshotSuffix = "-SNAPSHOT"
watcherMaxWaitTime = 30 * time.Second
fipsPrefix = "-fips"
)

var agentArtifact = artifact.Artifact{
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions pkg/control/v2/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
43 changes: 28 additions & 15 deletions pkg/testing/define/define.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"sync"
"testing"

"github.com/stretchr/testify/require"

"github.com/gofrs/uuid/v5"

"github.com/elastic/elastic-agent-libs/kibana"
Expand Down Expand Up @@ -83,33 +85,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" {
Expand Down Expand Up @@ -301,3 +301,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
}
23 changes: 16 additions & 7 deletions pkg/testing/fetcher_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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())
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions pkg/testing/fetcher_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestLocalFetcher(t *testing.T) {
tcs := []struct {
name string
version string
opts []localFetcherOpt
opts []LocalFetcherOpt
want []byte
wantHash []byte
}{
Expand All @@ -69,7 +69,7 @@ func TestLocalFetcher(t *testing.T) {
}, {
name: "SnapshotOnly",
version: baseVersion,
opts: []localFetcherOpt{WithLocalSnapshotOnly()},
opts: []LocalFetcherOpt{WithLocalSnapshotOnly()},
want: snapshotContent,
wantHash: snapshotContentHash,
}, {
Expand All @@ -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,
},
Expand Down
7 changes: 7 additions & 0 deletions pkg/testing/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Fixture struct {
binaryName string
runLength time.Duration
additionalArgs []string
fipsArtifact bool

srcPackage string
workDir string
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/version/version_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading