blob: aab55072b391710eee787d1519b39e4205ac98dd [file] [edit]
// Copyright 2024 Google LLC
//
// Licensed 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
//
// https://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.
package manager
import (
"context"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
"time"
acmpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/acs/client"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/resource"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/retry"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/run"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestAddress(t *testing.T) {
ctx := context.Background()
setConnectionsDir(t, "/tmp/socket_connections")
policy := retry.Policy{MaxAttempts: 3, BackoffFactor: 1, Jitter: time.Millisecond}
tests := []struct {
name string
plugin string
want string
}{
{
name: "unix",
plugin: "pluginA",
want: filepath.Clean("/tmp/socket_connections/pluginA.sock"),
},
{
name: "tcp",
plugin: "pluginB",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := address(ctx, tc.name, tc.plugin, policy)
if err != nil {
t.Fatalf("address(ctx, %v, %v, %v) failed unexpectedly with error: %v", tc.name, tc.plugin, policy, err)
}
if tc.name == "unix" {
if got != tc.want {
t.Errorf("address(ctx, %v, %v, %v) = %v, want %v", tc.plugin, tc.plugin, policy, got, tc.want)
}
return
}
// TCP, verify you can listen on the port address() returned.
l, err := net.Listen("tcp", got)
if err != nil {
t.Fatalf("net.Listen(tcp, %s) failed unexpectedly with error: %v", got, err)
}
l.Close()
})
}
}
func setConnectionsDir(t *testing.T, addr string) {
t.Helper()
if err := cfg.Load([]byte{}); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
config := cfg.Retrieve()
orig := config.Plugin.SocketConnectionsDir
config.Plugin.SocketConnectionsDir = addr
t.Cleanup(func() {
config.Plugin.SocketConnectionsDir = orig
})
}
func TestConnectionsPath(t *testing.T) {
if err := cfg.Load([]byte{}); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
want := map[string]string{
"windows": `C:\ProgramData\Google\Compute Engine\google-guest-agent`,
"linux": "/run/google-guest-agent/plugin-connections",
}[runtime.GOOS]
if got := connectionsPath(); got != want {
t.Errorf("connectionsPath() defaulted to = %q, want %q", got, want)
}
setConnectionsDir(t, "/tmp//socket_connections")
want = filepath.Clean("/tmp/socket_connections")
if got := connectionsPath(); got != want {
t.Errorf("connectionsPath() = %q, want overridden path %q", got, want)
}
}
type testRunner struct {
seenArguments []string
seenCommand string
pid int
shouldFail bool
}
func setupFakeRunner(t *testing.T) *testRunner {
t.Helper()
runner := &testRunner{}
origClient := run.Client
run.Client = runner
t.Cleanup(func() {
run.Client = origClient
})
return runner
}
func (tr *testRunner) WithContext(ctx context.Context, opts run.Options) (*run.Result, error) {
if opts.ExecMode == run.ExecModeAsync || opts.ExecMode == run.ExecModeDetach {
return tr.Start(ctx, opts)
}
return nil, nil
}
func (tr *testRunner) Start(ctx context.Context, opts run.Options) (*run.Result, error) {
if tr.shouldFail {
return nil, errors.New("test-start-errror")
}
tr.seenArguments = opts.Args
tr.seenCommand = opts.Name
return &run.Result{Pid: tr.pid}, nil
}
func TestLauncherStep(t *testing.T) {
stateDir := t.TempDir()
pluginInstalls := filepath.Join(stateDir, pluginInstallDir)
if err := os.MkdirAll(pluginInstalls, 0755); err != nil {
t.Fatalf("Failed to create test plugin install directory: %v", err)
}
entryPath := filepath.Join(t.TempDir(), "main")
step := launchStep{entryPath: entryPath, maxMemoryUsage: 10, maxCPUUsage: 20, startAttempts: 3, protocol: udsProtocol, extraArgs: []string{"--foo=bar"}}
ts := &testPluginServer{ctrs: make(map[string]int)}
addr := filepath.Join(t.TempDir(), "pluginA_revisionA.sock")
startTestServer(t, ts, udsProtocol, addr)
wantName := "LaunchPluginStep"
wantStatus := acmpb.CurrentPluginStates_STARTING
wantErrorStatus := acmpb.CurrentPluginStates_CRASHED
if step.Name() != wantName {
t.Errorf("launchStep.Name() = %s, want %s", step.Name(), wantName)
}
if step.Status() != wantStatus {
t.Errorf("launchStep.Status() = %s, want %s", step.Status(), wantStatus)
}
if step.ErrorStatus() != wantErrorStatus {
t.Errorf("launchStep.ErrorStatus() = %s, want %s", step.ErrorStatus(), wantErrorStatus)
}
setConnectionsDir(t, filepath.Dir(addr))
ctc := setupConstraintTestClient(t)
tr := setupFakeRunner(t)
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
wantMaxMemoryUsage := step.maxMemoryUsage
wantMaxCPUUsage := step.maxCPUUsage
cfg.Retrieve().Core.ACSClient = false
tests := []struct {
name string
path string
pluginInstallationType acmpb.PluginInstallationType
serviceCfg string
status acmpb.CurrentPluginStates_StatusValue
launchFail bool
shouldFail bool
}{
{
name: "success_core_plugin",
path: t.TempDir(),
pluginInstallationType: acmpb.PluginInstallationType_LOCAL_INSTALLATION,
status: acmpb.CurrentPluginStates_RUNNING,
},
{
name: "success_dynamic_plugin",
path: t.TempDir(),
pluginInstallationType: acmpb.PluginInstallationType_DYNAMIC_INSTALLATION,
status: acmpb.CurrentPluginStates_RUNNING,
},
{
name: "launch_failure",
path: t.TempDir(),
pluginInstallationType: acmpb.PluginInstallationType_DYNAMIC_INSTALLATION,
status: acmpb.CurrentPluginStates_CRASHED,
shouldFail: true,
launchFail: true,
},
{
name: "start_failure",
path: t.TempDir(),
serviceCfg: "error",
pluginInstallationType: acmpb.PluginInstallationType_DYNAMIC_INSTALLATION,
status: acmpb.CurrentPluginStates_CRASHED,
shouldFail: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
plugin := &Plugin{Name: "pluginA", Revision: "revisionA", RuntimeInfo: &RuntimeInfo{statusMu: sync.RWMutex{}}, Manifest: &Manifest{StartTimeout: time.Second * 5}}
wantPluginName := plugin.FullName()
t.Cleanup(func() {
plugin.setState(acmpb.CurrentPluginStates_STATE_VALUE_UNSPECIFIED)
})
cfg.Retrieve().Plugin.StateDir = stateDir
cfg.Retrieve().Plugin.SocketConnectionsDir = filepath.Dir(addr)
plugin.InstallPath = tc.path
plugin.Manifest.PluginInstallationType = tc.pluginInstallationType
wantArgs := []string{"--foo=bar", fmt.Sprintf("--protocol=%s", udsProtocol), fmt.Sprintf("--address=%s", addr), fmt.Sprintf("--errorlogfile=%s", plugin.logfile())}
plugin.Manifest.StartConfig = &ServiceConfig{Simple: tc.serviceCfg}
tr.shouldFail = tc.launchFail
err := step.Run(ctx, plugin)
if (err != nil) != tc.shouldFail {
t.Errorf("launchStep.Run(ctx, %+v) = error: %v, want error: %t", plugin, err, tc.shouldFail)
}
if got := plugin.State(); got != tc.status {
t.Errorf("launchStep.Run(ctx, %+v) = plugin state %s, want %s", plugin, got, tc.status)
}
// Test state was stored on successful run.
file := plugin.stateFile()
if !tc.shouldFail {
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
t.Errorf("launchStep.Run(ctx, %+v) did not write plugin state to file %s", plugin, file)
}
}
if tr.seenCommand != entryPath {
t.Errorf("launchStep.Run(ctx, %+v) executed %q, want %s ", plugin, tr.seenCommand, entryPath)
}
if diff := cmp.Diff(wantArgs, tr.seenArguments); diff != "" {
t.Errorf("launchStep.Run(ctx, %+v) executed unexpectedly with diff (-want +got):\n%s", plugin, diff)
}
wantPlugin := &Plugin{
Name: plugin.Name,
Revision: plugin.Revision,
Address: addr,
Manifest: &Manifest{
LaunchArguments: []string{"--foo=bar"},
MaxMemoryUsage: 10,
MaxCPUUsage: 20,
StartAttempts: 3,
StartTimeout: time.Second * 5,
StartConfig: &ServiceConfig{Simple: tc.serviceCfg},
PluginInstallationType: tc.pluginInstallationType,
},
EntryPath: entryPath,
RuntimeInfo: &RuntimeInfo{
Pid: tr.pid,
status: tc.status,
},
InstallPath: tc.path,
Protocol: step.protocol,
}
if diff := cmp.Diff(wantPlugin, plugin, cmpopts.IgnoreUnexported(Plugin{}, RuntimeInfo{}), cmpopts.IgnoreFields(Plugin{}, "client"), cmpopts.IgnoreUnexported(Manifest{})); diff != "" {
t.Errorf("launchStep.Run(ctx, %+v) executed unexpectedly with diff (-want +got):\n%s", plugin, diff)
}
if !tc.launchFail {
expectedConstraint := resource.Constraint{
Name: wantPluginName,
MaxMemoryUsage: wantMaxMemoryUsage,
MaxCPUUsage: wantMaxCPUUsage,
}
if diff := cmp.Diff(expectedConstraint, ctc.seenConstraint, cmpopts.IgnoreFields(resource.Constraint{}, "Name")); diff != "" {
t.Errorf("launchStep.Run(ctx, %+v) applied unexpected constraints (-want +got):\n%s", plugin, diff)
}
}
// Test symlink points to right target directory.
got, err := os.Readlink(plugin.staticInstallPath())
if tc.pluginInstallationType == acmpb.PluginInstallationType_LOCAL_INSTALLATION {
if err == nil {
t.Fatalf("os.Readlink(%s) succeeded, should have failed for core plugin", plugin.staticInstallPath())
}
} else {
if err != nil {
t.Fatalf("os.Readlink(%s) failed unexpectedly with error: %v", plugin.staticInstallPath(), err)
}
if got != wantPlugin.InstallPath {
t.Errorf("launchStep.Run(ctx, %+v) = symlink target %q, want %q", plugin, got, wantPlugin.InstallPath)
}
}
})
}
}
func TestConnectionSetup(t *testing.T) {
connectionsDir := filepath.Join(t.TempDir(), "agent-connections")
addr := filepath.Join(connectionsDir, "pluginA.sock")
if err := connectionSetup(addr); err != nil {
t.Fatalf("connectionSetup(%q) failed unexpectedly with error: %v", addr, err)
}
if !file.Exists(connectionsDir, file.TypeDir) {
t.Errorf("connectionSetup(%s) did not create %q directory", connectionsDir, addr)
}
if file.Exists(addr, file.TypeFile) {
t.Errorf("connectionSetup(%s) did not create %q directory", connectionsDir, addr)
}
}
func TestIsUDSSupported(t *testing.T) {
if err := cfg.Load([]byte{}); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
connectionsDir := filepath.Join(t.TempDir(), "agent-connections")
cfg.Retrieve().Plugin.SocketConnectionsDir = connectionsDir
testConnection := filepath.Join(connectionsDir, "test-connection")
if !isUDSSupported() {
t.Errorf("isUDSSupported() = false, want true")
}
if runtime.GOOS == "linux" {
return
}
if file.Exists(testConnection, file.TypeFile) {
t.Errorf("file.Exists(%s, file.TypeFile) = true, want false", testConnection)
}
if !file.Exists(connectionsDir, file.TypeDir) {
t.Errorf("file.Exists(%s, file.TypeDir) = true, want false", connectionsDir)
}
}