Skip to content

Commit 96918f7

Browse files
authored
Merge branch 'main' into fix-install-prevention-on-linux
2 parents 65c0b82 + 633272c commit 96918f7

19 files changed

+427
-60
lines changed

.buildkite/bk.integration.pipeline.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ steps:
4444
- fleet-privileged
4545
- standalone-upgrade
4646
- upgrade
47+
- upgrade-flavor
4748
- install-uninstall
4849

4950
- label: "Win2022:non-sudo:{{matrix}}"
@@ -95,6 +96,7 @@ steps:
9596
matrix:
9697
- default
9798
- upgrade
99+
- upgrade-flavor
98100
- standalone-upgrade
99101
- fleet
100102
- fleet-endpoint-security
@@ -122,6 +124,7 @@ steps:
122124
matrix:
123125
- default
124126
- upgrade
127+
- upgrade-flavor
125128
- standalone-upgrade
126129
- fleet
127130
- fleet-endpoint-security
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Kind can be one of:
2+
# - breaking-change: a change to previously-documented behavior
3+
# - deprecation: functionality that is being removed in a later release
4+
# - bug-fix: fixes a problem in a previous version
5+
# - enhancement: extends functionality but does not break or fix existing behavior
6+
# - feature: new functionality
7+
# - known-issue: problems that we are aware of in a given version
8+
# - security: impacts on the security of a product or a user’s deployment.
9+
# - upgrade: important information for someone upgrading from a prior version
10+
# - other: does not fit into any of the other categories
11+
kind: bug-fix
12+
13+
# Change summary; a 80ish characters long description of the change.
14+
summary: Change how Windows process handles are obtained when assigning sub-processes to Job objects.
15+
16+
# Long description; in case the summary is not enough to describe the change
17+
# this field accommodate a description without length limits.
18+
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
19+
#description:
20+
21+
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
22+
component: "elastic-agent"
23+
24+
# PR URL; optional; the PR number that added the changeset.
25+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
26+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
27+
# Please provide it if you are adding a fragment for a different PR.
28+
pr: https://github.com/owner/repo/6825
29+
30+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
31+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
32+
#issue: https://github.com/owner/repo/1234

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,19 +219,26 @@ func getPackageMetadataFromZip(archivePath string) (packageMetadata, error) {
219219
}
220220

221221
func skipFnFromZip(log *logger.Logger, r *zip.ReadCloser, detectedFlavor string, fileNamePrefix string, versionedHome string, registry map[string][]string) (install.SkipFn, error) {
222+
acceptAllFn := func(relPath string) bool { return false }
222223
if detectedFlavor == "" {
223224
// no flavor don't skip anything
224-
return func(relPath string) bool { return false }, nil
225+
return acceptAllFn, nil
225226
}
226227

227228
flavor, err := install.Flavor(detectedFlavor, "", registry)
228229
if err != nil {
229230
if errors.Is(err, install.ErrUnknownFlavor) {
230231
// unknown flavor fallback to copy all
231-
return func(relPath string) bool { return false }, nil
232+
return acceptAllFn, nil
232233
}
233234
return nil, err
234235
}
236+
237+
// no flavor loaded
238+
if flavor.Name == "" {
239+
return acceptAllFn, nil
240+
}
241+
235242
specsInFlavor := install.SpecsForFlavor(flavor) // ignoring error flavor exists, it was loaded before
236243

237244
// fix versionedHome
@@ -455,20 +462,13 @@ func untar(log *logger.Logger, archivePath, dataDir string, flavor string) (Unpa
455462
}
456463

457464
func skipFnFromTar(log *logger.Logger, archivePath string, flavor string, registry map[string][]string) (install.SkipFn, error) {
465+
acceptAllFn := func(relPath string) bool { return false }
458466
if flavor == "" {
459467
// no flavor don't skip anything
460-
return func(relPath string) bool { return false }, nil
468+
return acceptAllFn, nil
461469
}
462470

463471
fileNamePrefix := getFileNamePrefix(archivePath)
464-
loadFlavor := func(flavor string) install.FlavorDefinition {
465-
components, found := registry[flavor]
466-
if !found {
467-
return install.FlavorDefinition{}
468-
}
469-
470-
return install.FlavorDefinition{Name: flavor, Components: components}
471-
}
472472

473473
// scan tar archive for spec file and extract allowed paths
474474
r, err := os.Open(archivePath)
@@ -485,7 +485,20 @@ func skipFnFromTar(log *logger.Logger, archivePath string, flavor string, regist
485485
tr := tar.NewReader(zr)
486486

487487
var allowedPaths []string
488-
flavorDefinition := loadFlavor(flavor)
488+
flavorDefinition, err := install.Flavor(flavor, "", registry)
489+
if err != nil {
490+
if errors.Is(err, install.ErrUnknownFlavor) {
491+
// unknown flavor fallback to copy all
492+
return acceptAllFn, nil
493+
}
494+
return nil, err
495+
}
496+
497+
// no flavor loaded
498+
if flavorDefinition.Name == "" {
499+
return acceptAllFn, nil
500+
}
501+
489502
specs, err := specRegistry(flavorDefinition)
490503
if err != nil {
491504
return nil, err

internal/pkg/agent/cmd/install.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
"os/exec"
1212
"path/filepath"
1313
"runtime"
14+
"strings"
1415

1516
"github.com/spf13/cobra"
17+
"github.com/spf13/pflag"
1618

1719
"github.com/elastic/elastic-agent-libs/logp"
1820
"github.com/elastic/elastic-agent/internal/pkg/agent/application/filelock"
@@ -84,6 +86,11 @@ would like the Agent to operate.
8486
func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
8587
var err error
8688

89+
if installServers, _ := cmd.Flags().GetBool(flagInstallServers); isFleetServerFlagProvided(cmd) && !installServers {
90+
_ = cmd.Flags().Lookup(flagInstallServers).Value.Set("true") // this can fail only when parsing bool
91+
fmt.Fprintf(streams.Out, "fleet-server installation detected, using --%s flag\n", flagInstallServers)
92+
}
93+
8794
err = validateEnrollFlags(cmd)
8895
if err != nil {
8996
return fmt.Errorf("could not validate flags: %w", err)
@@ -356,6 +363,27 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
356363
return nil
357364
}
358365

366+
func isFleetServerFlagProvided(cmd *cobra.Command) bool {
367+
var fleetServerFlagPresent bool
368+
cmd.Flags().VisitAll(func(f *pflag.Flag) {
369+
if fleetServerFlagPresent {
370+
return
371+
}
372+
373+
if !strings.HasPrefix(f.Name, "fleet-server-") {
374+
return
375+
}
376+
377+
flag := cmd.Flags().Lookup(f.Name)
378+
if flag != nil && flag.Changed {
379+
fleetServerFlagPresent = true
380+
}
381+
382+
})
383+
384+
return fleetServerFlagPresent
385+
}
386+
359387
// execUninstall execs "elastic-agent uninstall --force" from the elastic agent installed on the system (found in PATH)
360388
func execUninstall(streams *cli.IOStreams, topPath string, binName string) error {
361389
args := []string{

internal/pkg/agent/install/flavors.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func UsedFlavor(topPath, defaultFlavor string) (string, error) {
4545
}
4646

4747
// failed reading flavor, do not break behavior and apply none as widest
48-
return "", err
48+
return "", fmt.Errorf("failed reading flavor marker file: %w", err)
4949
}
5050

5151
return string(content), nil
@@ -55,11 +55,15 @@ func Flavor(detectedFlavor string, registryPath string, flavorsRegistry map[stri
5555
if flavorsRegistry == nil {
5656
f, err := os.Open(registryPath)
5757
if err != nil {
58-
return FlavorDefinition{}, err
58+
if os.IsNotExist(err) {
59+
return FlavorDefinition{}, ErrUnknownFlavor
60+
}
61+
62+
return FlavorDefinition{}, fmt.Errorf("failed opening flavor registry: %w", err)
5963
}
6064
manifest, err := v1.ParseManifest(f)
6165
if err != nil {
62-
return FlavorDefinition{}, err
66+
return FlavorDefinition{}, fmt.Errorf("failed parsing flavor registry: %w", err)
6367
}
6468
defer f.Close()
6569
flavorsRegistry = manifest.Package.Flavors
@@ -110,12 +114,12 @@ func ApplyFlavor(versionedHome string, flavor FlavorDefinition) error {
110114
return nil
111115
})
112116
if err != nil {
113-
return err
117+
return fmt.Errorf("failed traversing components directory: %w", err)
114118
}
115119

116120
for _, ftr := range filesToRemove {
117121
if removeErr := os.RemoveAll(ftr); !os.IsNotExist(removeErr) {
118-
err = removeErr
122+
err = fmt.Errorf("failed cleaning components: %w", removeErr)
119123
}
120124
}
121125

@@ -222,7 +226,7 @@ func subpathsForComponent(componentName, sourceComponentsDir string) ([]string,
222226
if os.IsNotExist(err) {
223227
return nil, nil
224228
}
225-
return nil, err
229+
return nil, fmt.Errorf("failed reading spec file for component %q: %w", componentName, err)
226230
}
227231

228232
return component.ParseComponentFiles(content, specFilename, true)

pkg/core/process/external_windows.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"os"
1111
"syscall"
1212
"time"
13+
14+
"golang.org/x/sys/windows"
1315
)
1416

1517
const (
@@ -34,16 +36,17 @@ func externalProcess(proc *os.Process) {
3436

3537
func isWindowsProcessExited(pid int) bool {
3638
const desiredAccess = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE
37-
h, err := syscall.OpenProcess(desiredAccess, false, uint32(pid))
39+
h, err := windows.OpenProcess(desiredAccess, false, uint32(pid)) //nolint:gosec // G115 Conversion from int to uint32 is safe here.
3840
if err != nil {
3941
// failed to open handle, report exited
4042
return true
4143
}
44+
defer windows.CloseHandle(h) //nolint:errcheck // No way to handle errors returned here so safe to ignore.
4245

4346
// get exit code, this returns immediately in case it is still running
4447
// it returns exitCodeStillActive
4548
var ec uint32
46-
if err := syscall.GetExitCodeProcess(h, &ec); err != nil {
49+
if err := windows.GetExitCodeProcess(h, &ec); err != nil {
4750
// failed to contact, report exited
4851
return true
4952
}

pkg/core/process/job_windows.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package process
88

99
import (
10+
"fmt"
1011
"os"
1112
"unsafe"
1213

@@ -76,12 +77,23 @@ func (job Job) Assign(p *os.Process) error {
7677
if job == 0 || p == nil {
7778
return nil
7879
}
79-
return windows.AssignProcessToJobObject(
80-
windows.Handle(job),
81-
windows.Handle((*process)(unsafe.Pointer(p)).Handle))
82-
}
8380

84-
type process struct {
85-
Pid int
86-
Handle uintptr
81+
// To assign a process to a job, you need a handle to the process. Since os.Process provides no
82+
// way to obtain it's underlying handle safely, get one with OpenProcess().
83+
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
84+
// This requires at least the PROCESS_SET_QUOTA and PROCESS_TERMINATE access rights.
85+
// https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-assignprocesstojobobject
86+
desiredAccess := uint32(windows.PROCESS_SET_QUOTA | windows.PROCESS_TERMINATE)
87+
processHandle, err := windows.OpenProcess(desiredAccess, false, uint32(p.Pid)) //nolint:gosec // G115 Conversion from int to uint32 is safe here.
88+
if err != nil {
89+
return fmt.Errorf("opening process handle: %w", err)
90+
}
91+
defer windows.CloseHandle(processHandle) //nolint:errcheck // No way to handle errors returned here so safe to ignore.
92+
93+
err = windows.AssignProcessToJobObject(windows.Handle(job), processHandle)
94+
if err != nil {
95+
return fmt.Errorf("assigning to job object: %w", err)
96+
}
97+
98+
return nil
8799
}

pkg/testing/fixture.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ func (f *Fixture) ExecStatus(ctx context.Context, opts ...process.CmdOption) (Ag
726726
status := AgentStatusOutput{}
727727
if uerr := json.Unmarshal(out, &status); uerr != nil {
728728
return AgentStatusOutput{},
729-
fmt.Errorf("could not unmarshal agent status output: %w", errors.Join(uerr, err))
729+
fmt.Errorf("could not unmarshal agent status output: %w:\n%s", errors.Join(uerr, err), out)
730730
} else if status.IsZero() {
731731
return status, fmt.Errorf("agent status output is empty: %w", err)
732732
}

pkg/testing/fixture_install.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (f *Fixture) installFunc(ctx context.Context, installOpts *InstallOpts, sho
200200

201201
// check for running agents before installing, but only if not installed into a namespace whose point is allowing two agents at once.
202202
if installOpts != nil && !installOpts.Develop && installOpts.Namespace == "" {
203-
assert.Empty(f.t, getElasticAgentProcesses(f.t), "there should be no running agent at beginning of Install()")
203+
assert.Empty(f.t, getElasticAgentAndAgentbeatProcesses(f.t), "there should be no running agent at beginning of Install()")
204204
}
205205

206206
switch f.packageFormat {
@@ -287,19 +287,22 @@ func (f *Fixture) installNoPkgManager(ctx context.Context, installOpts *InstallO
287287

288288
f.t.Cleanup(func() {
289289
// check for running agents after uninstall had a chance to run
290-
processes := getElasticAgentProcesses(f.t)
291-
292290
// there can be a single agent left when using --develop mode
293291
if f.installOpts != nil && f.installOpts.Namespace != "" {
294-
assert.LessOrEqualf(f.t, len(processes), 1, "More than one agent left running at the end of the test when second agent in namespace %s was used: %v", f.installOpts.Namespace, processes)
292+
// Only consider the main agent process and not sub-processes so that we can detect when
293+
// multiple agents are running without needing to know the number of input sub-processes to expect.
294+
agentProcesses := getElasticAgentProcesses(f.t)
295+
assert.LessOrEqualf(f.t, len(agentProcesses), 1, "More than one agent left running at the end of the test when second agent in namespace %s was used: %v", f.installOpts.Namespace, agentProcesses)
295296
// The agent left running has to be the non-development agent. The development agent should be uninstalled first as a convention.
296-
if len(processes) > 0 {
297-
assert.NotContainsf(f.t, processes[0].Cmdline, paths.InstallDirNameForNamespace(f.installOpts.Namespace),
298-
"The agent installed into namespace %s was left running at the end of the test or was not uninstalled first: %v", f.installOpts.Namespace, processes)
297+
if len(agentProcesses) > 0 {
298+
assert.NotContainsf(f.t, agentProcesses[0].Cmdline, paths.InstallDirNameForNamespace(f.installOpts.Namespace),
299+
"The agent installed into namespace %s was left running at the end of the test or was not uninstalled first: %v", f.installOpts.Namespace, agentProcesses)
299300
}
300301
return
301302
}
302303

304+
// If not using an installation namespace, there should be no elastic-agent or agentbeat processes left running.
305+
processes := getElasticAgentAndAgentbeatProcesses(f.t)
303306
assert.Empty(f.t, processes, "there should be no running agent at the end of the test")
304307
})
305308

@@ -409,6 +412,13 @@ func getElasticAgentProcesses(t *gotesting.T) []runningProcess {
409412
return getProcesses(t, `.*elastic\-agent.*`)
410413
}
411414

415+
// Includes both the main elastic-agent process and the agentbeat sub-processes for ensuring
416+
// that no sub-processes are orphaned from their parent process and left running. This
417+
// primarily tests that Windows Job Object assignment works.
418+
func getElasticAgentAndAgentbeatProcesses(t *gotesting.T) []runningProcess {
419+
return getProcesses(t, `.*(elastic\-agent|agentbeat).*`)
420+
}
421+
412422
func getProcesses(t *gotesting.T, regex string) []runningProcess {
413423
procStats := agentsystemprocess.Stats{
414424
Procs: []string{regex},

pkg/testing/tools/check/check.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func ConnectedToFleet(ctx context.Context, t *testing.T, fixture *integrationtes
4747
// for use with assert.Eventually or require.Eventually.
4848
func FleetAgentStatus(ctx context.Context,
4949
t *testing.T,
50+
fixture *integrationtest.Fixture,
5051
client *kibana.Client,
5152
policyID,
5253
expectedStatus string) func() bool {
@@ -61,7 +62,13 @@ func FleetAgentStatus(ctx context.Context,
6162
return true
6263
}
6364

64-
t.Logf("Agent fleet status: %s", currentStatus)
65+
agentStatus, err := fixture.ExecStatus(ctx)
66+
if err != nil {
67+
t.Logf("Agent fleet status: %s Error getting local status: %s", currentStatus, err)
68+
return false
69+
}
70+
71+
t.Logf("Agent fleet status: %s Local status: %v", currentStatus, agentStatus)
6572
return false
6673
}
6774
}

0 commit comments

Comments
 (0)