Skip to content

Adding test for enrolling local FIPS agent into ECH FIPS Fleet Server #8197

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 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
2 changes: 1 addition & 1 deletion testing/integration/beats_serverless_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
85 changes: 85 additions & 0 deletions testing/integration/enroll_fips_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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.

//go:build integration

package integration

import (
"context"
"testing"
"time"

"github.com/elastic/elastic-agent-libs/kibana"
"github.com/elastic/elastic-agent-libs/testing/estools"
atesting "github.com/elastic/elastic-agent/pkg/testing"
"github.com/elastic/elastic-agent/pkg/testing/define"
"github.com/elastic/elastic-agent/pkg/testing/tools"

"github.com/gofrs/uuid/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestEnrollFIPS enrolls the locally-built FIPS-capable Elastic Agent into a
// FIPS-capable Fleet Server running in ECH, adds an integration
// to this Agent, and checks that data from the integration shows up in Elasticsearch.
// This test proves that it's possible for a local (on-prem) FIPS-capable Elastic Agent
// to enroll into a FIPS-capable Fleet Server (aka Integrations Server) running in ECH,
// while also exercising the connection between Fleet and the Elastic Package Registry (EPR)
// and also ensuring that the data path from Agent to Elasticsearch works as well.
func TestEnrollFIPS(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,
})

// Select FIPS-capable local Agent artifact
fixture, err := define.NewFixtureFromLocalFIPSBuild(t, define.Version())
require.NoError(t, err)

// Enroll Agent
policyUUID := uuid.Must(uuid.NewV4()).String()
basePolicy := kibana.AgentPolicy{
Name: "test-policy-" + policyUUID,
Namespace: "default",
Description: "Test policy " + policyUUID,
MonitoringEnabled: []kibana.MonitoringEnabledOption{
kibana.MonitoringEnabledLogs,
kibana.MonitoringEnabledMetrics,
},
}

installOpts := atesting.InstallOpts{
NonInteractive: true,
Force: true,
Privileged: true,
}

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()

policyResp, _, err := tools.InstallAgentWithPolicy(ctx, t, installOpts, fixture, info.KibanaClient, basePolicy)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the part that defines this enrollment to happen to the Fleet Server as part of the Integrations Server on ECH? I checked the code and understand it is enrolling to the default Fleet Server, but I don't easily spot where the connection to Integrations Server is made.

require.NoError(t, err)

// Install system integration
_, err = tools.InstallPackageFromDefaultFile(ctx, info.KibanaClient, "system", preinstalledPackages["system"], "system_integration_setup.json", uuid.Must(uuid.NewV4()).String(), policyResp.ID)
require.NoError(t, err)

// Ensure data from system integration shows up in Elasticsearch
status, err := fixture.ExecStatus(ctx)
require.NoError(t, err)

docs, err := estools.GetResultsForAgentAndDatastream(ctx, info.ESClient, "system.cpu", status.Info.ID)
assert.NoError(t, err, "error fetching system metrics")
assert.Greater(t, docs.Hits.Total.Value, 0, "could not find any matching system metrics for agent ID %s", status.Info.ID)
t.Logf("Generated %d system events", docs.Hits.Total.Value)

}