Skip to content
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

Copy Java attacher jar to a tmp directory #8803

Merged
merged 29 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0f1c5c2
Copy Java attacher jar to a tmp directory
eyalkoren Aug 4, 2022
ee2a3fc
Fix tests on Windows
eyalkoren Aug 9, 2022
43961a0
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Aug 10, 2022
2bec8cd
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Aug 21, 2022
1b5182e
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Aug 23, 2022
48fb9e5
Create tmp dir per user with proper dir and jar modes
eyalkoren Aug 23, 2022
99c6e72
Defer file close and ignore errors
eyalkoren Aug 23, 2022
656f3c2
Avoid using non-Windows attacher method in test
eyalkoren Aug 23, 2022
d3cf8e7
Fix test for Windows
eyalkoren Aug 24, 2022
9af7a03
Use Atoi
eyalkoren Aug 24, 2022
79062fe
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Aug 24, 2022
7ddf938
Applying revierw changes
eyalkoren Aug 24, 2022
85a3d0a
Replace javaw.exe with java.exe
eyalkoren Aug 24, 2022
1faede3
go doc changes
eyalkoren Aug 25, 2022
8d42b10
remove redundant variable
eyalkoren Aug 25, 2022
a7589e8
Improving set user
eyalkoren Aug 25, 2022
42da18e
remove IDE instruction
eyalkoren Aug 25, 2022
553755e
Merge remote-tracking branch 'eyalkoren/java-attacher-copy-to-temp' i…
eyalkoren Aug 25, 2022
0a7e92c
Adding missing return statement
eyalkoren Aug 25, 2022
97e29b8
Separate tests into OS-specific files
eyalkoren Aug 25, 2022
d421b68
Remove redundant slice initialization
eyalkoren Aug 25, 2022
70b1655
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Aug 25, 2022
09cb624
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Sep 7, 2022
1442e5a
Adding go doc
eyalkoren Sep 7, 2022
8869366
Merge remote-tracking branch 'eyalkoren/java-attacher-copy-to-temp' i…
eyalkoren Sep 7, 2022
f304f4d
Applying review changes
eyalkoren Sep 7, 2022
865e5c3
Merge remote-tracking branch 'upstream/main' into java-attacher-copy-…
eyalkoren Sep 7, 2022
500f845
Adding to changelog
eyalkoren Sep 7, 2022
fb8f0ef
Update changelogs/head.asciidoc
eyalkoren Sep 7, 2022
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
45 changes: 34 additions & 11 deletions internal/beater/java_attacher/java_attacher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
Expand All @@ -40,8 +39,10 @@ import (
"github.com/elastic/apm-server/internal/beater/config"
)

// javaAttacher is bundled by the server
var javaAttacher = filepath.FromSlash("./java-attacher.jar")
const javawExe = "javaw.exe"
const javaExe = "java.exe"

const bundledJavaAttacher = "java-attacher.jar"

type jvmDetails struct {
user string
Expand All @@ -62,11 +63,14 @@ type JavaAttacher struct {
agentConfigs map[string]string
downloadAgentVersion string
jvmCache map[int]*jvmDetails
uidToAttacherJar map[string]string
tmpDirs []string
tmpAttacherLock sync.Mutex
}

func New(cfg config.JavaAttacherConfig) (*JavaAttacher, error) {
logger := logp.NewLogger("java-attacher")
if _, err := os.Stat(javaAttacher); err != nil {
if _, err := os.Stat(bundledJavaAttacher); err != nil {
return nil, err
}
if !cfg.Enabled {
Expand All @@ -83,6 +87,7 @@ func New(cfg config.JavaAttacherConfig) (*JavaAttacher, error) {
downloadAgentVersion: cfg.DownloadAgentVersion,
rawDiscoveryRules: cfg.DiscoveryRules,
jvmCache: make(map[int]*jvmDetails),
uidToAttacherJar: make(map[string]string),
}
for _, flag := range cfg.DiscoveryRules {
for name, value := range flag {
Expand Down Expand Up @@ -145,7 +150,7 @@ func (j *JavaAttacher) Run(ctx context.Context) error {
return fmt.Errorf("java attacher is disabled")
}

// Non-Windows: run discovery and attachment until context is closed.
defer j.cleanResources()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
Expand All @@ -160,14 +165,14 @@ func (j *JavaAttacher) Run(ctx context.Context) error {
j.logger.Errorf("error during JVMs discovery: %v", err)
continue
}
if err := j.foreachJVM(ctx, jvms, j.attachJVM, true, 30*time.Second); err != nil {
if err := j.foreachJVM(ctx, jvms, j.attachJVM, false, 2*time.Minute); err != nil {
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
// Error is non-fatal; try again next time.
j.logger.Errorf("JVM attachment failed: %s", err)
}
}
}

// this function blocks until discovery has ended, an error occurred or the context had been cancelled
// discoverJVMsForAttachment blocks until discovery ends, an error is received or the context is done.
func (j *JavaAttacher) discoverJVMsForAttachment(ctx context.Context) (map[int]*jvmDetails, error) {
jvms, err := j.discoverAllRunningJavaProcesses()
if err != nil {
Expand Down Expand Up @@ -253,14 +258,22 @@ func (j *JavaAttacher) discoverAllRunningJavaProcesses() (map[int]*jvmDetails, e
gid: gid,
pid: pid,
startTime: info.StartTime,
command: info.Exe,
command: normalizeJavaCommand(info.Exe),
version: "unknown", // filled in later
cmdLineArgs: strings.Join(info.Args, " "),
}
}
return jvms, nil
}

func normalizeJavaCommand(command string) string {
if strings.HasSuffix(command, javawExe) {
command = strings.TrimSuffix(command, javawExe)
command = command + javaExe
}
return command
}

// foreachJVM calls f for each JVM in jvms concurrently,
// returning when one of the following is true:
// 1. The function completes for each JVM
Expand Down Expand Up @@ -308,8 +321,8 @@ func (j *JavaAttacher) foreachJVM(

func (j *JavaAttacher) filterCached(jvms map[int]*jvmDetails) {
for pid, jvm := range jvms {
cachedjvmDetails, found := j.jvmCache[pid]
if !found || !cachedjvmDetails.startTime.Equal(jvm.startTime) {
cachedJvmDetails, found := j.jvmCache[pid]
if !found || !cachedJvmDetails.startTime.Equal(jvm.startTime) {
// this is a JVM not yet encountered - add to cache
j.jvmCache[pid] = jvm
} else {
Expand Down Expand Up @@ -354,15 +367,25 @@ func (j *JavaAttacher) verifyJVMExecutable(ctx context.Context, jvm *jvmDetails)
// once the agent is attached or the context is closed; it may be blocking for long and should be called with a timeout context.
func (j *JavaAttacher) attachJVM(ctx context.Context, jvm *jvmDetails) error {
cmd := j.attachJVMCommand(ctx, jvm)
if cmd == nil {
return nil
}
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
if err := j.setRunAsUser(jvm, cmd); err != nil {
j.logger.Warnf("Failed to attach as user %q: %v. Trying to attach as current user,", jvm.user, err)
}
return runAttacherCommand(ctx, cmd, j.logger)
}

// attachJVMCommand constructs an attacher command for the provided jvmDetails.
// NOTE: this method may have side effects, including the creation of a tmp directory with a copy of the attacher jar (non-Windows),
// as well as a corresponding status change to this JavaAttacher, where the created tmp dir and jar paths are cached.
func (j *JavaAttacher) attachJVMCommand(ctx context.Context, jvm *jvmDetails) *exec.Cmd {
attacherJar := j.getAttacherJar(jvm.uid)
if attacherJar == "" {
return nil
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
}
axw marked this conversation as resolved.
Show resolved Hide resolved
args := []string{
"-jar", javaAttacher,
"-jar", attacherJar,
"--log-level", "debug",
"--include-pid", strconv.Itoa(jvm.pid),
}
Expand Down
148 changes: 148 additions & 0 deletions internal/beater/java_attacher/java_attacher_nonwindows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build !windows
// +build !windows

package javaattacher

import (
"context"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBuildInvalidCommand(t *testing.T) {
cfg := createTestConfig()
f, err := os.Create(bundledJavaAttacher)
require.NoError(t, err)
//goland:noinspection GoUnhandledErrorResult
defer os.Remove(f.Name())

attacher, err := New(cfg)
require.NoError(t, err)
defer attacher.cleanResources()

// invalid user and group ensures usage of the bundled jar
jvm := &jvmDetails{
pid: 12345,
uid: "invalid",
command: filepath.FromSlash("/home/someuser/java_home/bin/java"),
}
command := attacher.attachJVMCommand(context.Background(), jvm)
assert.Nil(t, command)
assert.Empty(t, attacher.tmpDirs)
assert.Len(t, attacher.uidToAttacherJar, 1)
}

func TestBuildCommandWithTempJar(t *testing.T) {
cfg := createTestConfig()
f, err := os.Create(bundledJavaAttacher)
require.NoError(t, err)
//goland:noinspection GoUnhandledErrorResult
defer os.Remove(f.Name())

attacher, err := New(cfg)
require.NoError(t, err)
defer attacher.cleanResources()

currentUser, _ := user.Current()
jvm := &jvmDetails{
pid: 12345,
uid: currentUser.Uid,
gid: currentUser.Gid,
command: filepath.FromSlash("/home/someuser/java_home/bin/java"),
}
assert.Empty(t, attacher.tmpDirs)
assert.Empty(t, attacher.uidToAttacherJar)
command := attacher.attachJVMCommand(context.Background(), jvm)
assert.Len(t, attacher.tmpDirs, 1)
assert.Len(t, attacher.uidToAttacherJar, 1)
attacherJar := attacher.uidToAttacherJar[currentUser.Uid]
assert.NotEqual(t, attacherJar, "")
require.NotEqual(t, bundledJavaAttacher, attacherJar)
want := filepath.FromSlash(fmt.Sprintf("/home/someuser/java_home/bin/java -jar %v", attacherJar)) +
" --log-level debug --include-pid 12345 --download-agent-version 1.27.0 --config server_url=http://myhost:8200"

cmdArgs := strings.Join(command.Args, " ")
assert.Equal(t, want, cmdArgs)

cfg.Config["service_name"] = "my-cool-service"
attacher, err = New(cfg)
require.NoError(t, err)
defer attacher.cleanResources()

command = attacher.attachJVMCommand(context.Background(), jvm)
cmdArgs = strings.Join(command.Args, " ")
assert.Contains(t, cmdArgs, "--config server_url=http://myhost:8200")
assert.Contains(t, cmdArgs, "--config service_name=my-cool-service")
}

func TestTempDirCreation(t *testing.T) {
cfg := createTestConfig()
f, err := os.Create(bundledJavaAttacher)
require.NoError(t, err)
//goland:noinspection GoUnhandledErrorResult
defer os.Remove(f.Name())
attacher, err := New(cfg)
require.NoError(t, err)
defer attacher.cleanResources()

currentUser, _ := user.Current()
jvm := &jvmDetails{
pid: 12345,
uid: currentUser.Uid,
gid: currentUser.Gid,
command: filepath.FromSlash("/home/someuser/java_home/bin/java"),
}
assert.Empty(t, attacher.tmpDirs)
assert.Empty(t, attacher.uidToAttacherJar)
// this call creates the temp dir
attacher.attachJVMCommand(context.Background(), jvm)
assert.Len(t, attacher.uidToAttacherJar, 1)
attacherJar := attacher.uidToAttacherJar[currentUser.Uid]
assert.NotEqual(t, attacherJar, "")
require.NotEqual(t, bundledJavaAttacher, attacherJar)

assert.Len(t, attacher.tmpDirs, 1)
tempAttacherDir := attacher.tmpDirs[0]
require.DirExists(t, tempAttacherDir)
attacherDirInfo, err := os.Stat(tempAttacherDir)
require.NoError(t, err)
require.Equal(t, os.FileMode(0700), attacherDirInfo.Mode().Perm())
tempDir, err := os.Open(tempAttacherDir)
require.NoError(t, err)
files, err := tempDir.ReadDir(0)
require.NoError(t, err)
require.Len(t, files, 1)
attacherJarFileInfo, err := files[0].Info()
require.NoError(t, err)
require.Equal(t, os.FileMode(0600), attacherJarFileInfo.Mode().Perm())
require.Equal(t, attacherJar, filepath.Join(tempAttacherDir, attacherJarFileInfo.Name()))

// verify caching
attacher.attachJVMCommand(context.Background(), jvm)
assert.Len(t, attacher.tmpDirs, 1)
assert.Len(t, attacher.uidToAttacherJar, 1)
}
46 changes: 10 additions & 36 deletions internal/beater/java_attacher/java_attacher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@
package javaattacher

import (
"context"
"os"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -39,37 +36,6 @@ func TestNoAttacherCreatedWithoutDiscoveryRules(t *testing.T) {
require.Error(t, err)
}

func TestBuild(t *testing.T) {
cfg := createTestConfig()
f, err := os.Create(javaAttacher)
require.NoError(t, err)
//goland:noinspection GoUnhandledErrorResult
defer os.Remove(f.Name())

attacher, err := New(cfg)
require.NoError(t, err)

jvm := &jvmDetails{
pid: 12345,
command: filepath.FromSlash("/home/someuser/java_home/bin/java"),
}
cmd := attacher.attachJVMCommand(context.Background(), jvm)
want := filepath.FromSlash("/home/someuser/java_home/bin/java -jar ./java-attacher.jar") +
" --log-level debug --include-pid 12345 --download-agent-version 1.27.0 --config server_url=http://myhost:8200"

cmdArgs := strings.Join(cmd.Args, " ")
assert.Equal(t, want, cmdArgs)

cfg.Config["service_name"] = "my-cool-service"
attacher, err = New(cfg)
require.NoError(t, err)

cmd = attacher.attachJVMCommand(context.Background(), jvm)
cmdArgs = strings.Join(cmd.Args, " ")
assert.Contains(t, cmdArgs, "--config server_url=http://myhost:8200")
assert.Contains(t, cmdArgs, "--config service_name=my-cool-service")
}

func createTestConfig() config.JavaAttacherConfig {
args := []map[string]string{
{"exclude-user": "root"},
Expand Down Expand Up @@ -99,12 +65,13 @@ func TestDiscoveryRulesAllowlist(t *testing.T) {
Enabled: true,
DiscoveryRules: args,
}
f, err := os.Create(javaAttacher)
f, err := os.Create(bundledJavaAttacher)
require.NoError(t, err)
//goland:noinspection GoUnhandledErrorResult
defer os.Remove(f.Name())
javaAttacher, err := New(cfg)
require.NoError(t, err)
defer javaAttacher.cleanResources()
discoveryRules := javaAttacher.discoveryRules
require.Len(t, discoveryRules, allowlistLength)
}
Expand All @@ -125,12 +92,13 @@ func TestConfig(t *testing.T) {
},
DownloadAgentVersion: "1.25.0",
}
f, err := os.Create(javaAttacher)
f, err := os.Create(bundledJavaAttacher)
require.NoError(t, err)
//goland:noinspection GoUnhandledErrorResult
defer os.Remove(f.Name())
javaAttacher, err := New(cfg)
require.NoError(t, err)
defer javaAttacher.cleanResources()
require.True(t, javaAttacher.enabled)
require.Equal(t, "http://localhost:8200", javaAttacher.agentConfigs["server_url"])
require.Equal(t, "1.25.0", javaAttacher.downloadAgentVersion)
Expand Down Expand Up @@ -181,3 +149,9 @@ func TestConfig(t *testing.T) {
javaAttacher.discoveryRules[4] = &userDiscoveryRule{}
require.Nil(t, javaAttacher.findFirstMatch(&jvmDetails))
}

func TestJavaCommandNormalization(t *testing.T) {
javaBin := "java_home/bin/"
assert.Equal(t, javaBin+javaExe, normalizeJavaCommand(javaBin+javaExe))
assert.Equal(t, javaBin+javaExe, normalizeJavaCommand(javaBin+javawExe))
}
Loading