blob: 8a57f811e6502d488ec8588aad54458f0f610901 [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"
"crypto/sha256"
"encoding/hex"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
dpb "google.golang.org/protobuf/types/known/durationpb"
structpb "google.golang.org/protobuf/types/known/structpb"
tpb "google.golang.org/protobuf/types/known/timestamppb"
pb "github.com/GoogleCloudPlatform/agentcommunication_client/gapic/agentcommunicationpb"
acpb "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/boundedlist"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/command"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/events"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/resource"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/retry"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/scheduler"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)
type testConstraintClient struct {
seenName string
seenConstraint resource.Constraint
}
func (c *testConstraintClient) Apply(constraint resource.Constraint) error {
c.seenConstraint = constraint
return nil
}
func (c *testConstraintClient) RemoveConstraint(ctx context.Context, name string) error {
c.seenName = name
return nil
}
func (c *testConstraintClient) NewOOMWatcher(context.Context, resource.Constraint, time.Duration) (events.Watcher, error) {
return nil, nil
}
func setupConstraintTestClient(t *testing.T) *testConstraintClient {
oldClient := resource.Client
newClient := &testConstraintClient{}
resource.Client = newClient
t.Cleanup(func() { resource.Client = oldClient })
return newClient
}
func TestStore(t *testing.T) {
stateDir := t.TempDir()
setBaseStateDir(t, stateDir)
infoDir := filepath.Join(stateDir, agentStateDir, pluginInfoDir)
if err := os.MkdirAll(infoDir, 0755); err != nil {
t.Fatalf("os.MkdirAll(%s) failed to create test directories with error: %v", infoDir, err)
}
// Create a temporary directory for the test that must be ignored by Load().
_, err := os.MkdirTemp(infoDir, "test")
if err != nil {
t.Fatalf("os.MkdirTemp(%s) failed to create test directory with error: %v", stateDir, err)
}
wantStructCfg := &structpb.Struct{
Fields: map[string]*structpb.Value{
"name": structpb.NewStringValue("Abcd"),
"age": structpb.NewNumberValue(99),
"is_admin": structpb.NewBoolValue(true),
"places": structpb.NewListValue(&structpb.ListValue{Values: []*structpb.Value{structpb.NewStringValue("Washington"), structpb.NewStringValue("California")}}),
},
}
bytes, err := proto.Marshal(wantStructCfg)
if err != nil {
t.Fatalf("proto.Marshal(%+v) failed unexpectedly with error: %v", wantStructCfg, err)
}
cfg := &ServiceConfig{Simple: "simple config"}
cfg2 := &ServiceConfig{Structured: bytes}
p1 := &Plugin{
Name: "pluginA",
Revision: "1",
Address: "test-address1",
Protocol: "tcp",
Manifest: &Manifest{
StartAttempts: 3,
StartConfig: cfg,
PluginInstallationType: acpb.PluginInstallationType_LOCAL_INSTALLATION,
},
RuntimeInfo: &RuntimeInfo{Pid: 123},
}
p2 := &Plugin{
Name: "pluginB",
Revision: "2",
Address: "test-address2",
Manifest: &Manifest{
MaxMemoryUsage: 1024 * 1024,
StopTimeout: 3 * time.Second,
PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION,
},
RuntimeInfo: &RuntimeInfo{Pid: 123},
}
p3 := &Plugin{
Name: "pluginC",
Revision: "3",
Address: "test-address3",
Protocol: "tcp",
Manifest: &Manifest{
StartAttempts: 3,
StartConfig: cfg2,
PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION,
},
RuntimeInfo: &RuntimeInfo{Pid: 123},
}
corePlugin := &Plugin{
Name: CorePluginName,
Revision: "1",
Address: "test-address1",
Manifest: &Manifest{
StartAttempts: 5,
StartTimeout: time.Second * 30,
StopTimeout: time.Second * 30,
},
RuntimeInfo: &RuntimeInfo{Pid: 123},
}
wantCorePlugin := &Plugin{
Name: CorePluginName,
Revision: "1",
Address: "test-address1",
Manifest: &Manifest{
StartAttempts: 5,
StartTimeout: time.Second * 30,
StopTimeout: time.Second * 30,
PluginType: acpb.PluginType_DAEMON,
PluginInstallationType: acpb.PluginInstallationType_LOCAL_INSTALLATION,
},
RuntimeInfo: &RuntimeInfo{Pid: 123},
}
storeTests := []struct {
name string
plugin *Plugin
}{
{
name: "PluginA_store",
plugin: p1,
},
{
name: "PluginB_store",
plugin: p2,
},
{
name: "PluginC_store",
plugin: p3,
},
{
name: "core_plugin_store",
plugin: corePlugin,
},
}
for _, tc := range storeTests {
t.Run(tc.name, func(t *testing.T) {
if err := tc.plugin.Store(); err != nil {
t.Fatalf("plugin.Store() failed unexpectedly for [%+v] with error: %v", tc.plugin, err)
}
})
}
got, err := load(infoDir)
if err != nil {
t.Fatalf("load(%s) failed unexpectedly with error: %v", infoDir, err)
}
loadTests := []struct {
name string
plugin *Plugin
wantCfg *ServiceConfig
}{
{
name: "PluginA_load",
plugin: p1,
wantCfg: cfg,
},
{
name: "PluginB_load",
plugin: p2,
},
{
name: "PluginC_load",
plugin: p3,
wantCfg: cfg2,
},
{
name: "core_plugin_load",
plugin: wantCorePlugin,
},
}
for _, tc := range loadTests {
t.Run(tc.name, func(t *testing.T) {
gotP := got[tc.plugin.Name]
if diff := cmp.Diff(tc.plugin, gotP, cmpopts.IgnoreUnexported(Plugin{}, RuntimeInfo{}, Manifest{})); diff != "" {
t.Errorf("load(%s) returned diff (-want +got):\n%s", infoDir, diff)
}
if tc.wantCfg == nil || len(tc.wantCfg.Structured) == 0 {
return
}
// Tests bytes were correctly stored and can be parsed.
gotCfg, err := gotP.Manifest.StartConfig.toProto()
if err != nil {
t.Fatalf("%s plugin.toProto() failed unexpectedly with error: %v", gotP.FullName(), err)
}
if diff := cmp.Diff(wantStructCfg, gotCfg, protocmp.Transform()); diff != "" {
t.Errorf("load(%s) returned config diff (-want +got):\n%s", gotP.FullName(), diff)
}
})
}
// Non-existing state dir should not cause error.
nonExisting := filepath.Join(stateDir, "non-existing")
got, err = load(nonExisting)
if err != nil {
t.Fatalf("load(%s) failed unexpectedly with error: %v", infoDir, err)
}
if len(got) != 0 {
t.Errorf("load(%s) = %v, want empty plugin map", nonExisting, got)
}
}
func TestConnectOrReLaunch(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
setConnectionsDir(t, "")
setupConstraintTestClient(t)
tests := []struct {
desc string
wantCmd string
isRunning bool
}{
{
desc: "reconnect",
isRunning: true,
},
{
desc: "launch",
isRunning: false,
wantCmd: "testentry/binary",
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
var wantArgs []string
addr := filepath.Join(t.TempDir(), "pluginA_revisionA.sock")
startTestServer(t, &testPluginServer{statusFail: !tc.isRunning, ctrs: make(map[string]int)}, udsProtocol, addr)
cfg.Retrieve().Plugin.StateDir = t.TempDir()
cfg.Retrieve().Plugin.SocketConnectionsDir = filepath.Dir(addr)
// Setup install directory.
insallDir := filepath.Join(cfg.Retrieve().Plugin.StateDir, pluginInstallDir)
if err := os.MkdirAll(insallDir, 0755); err != nil {
t.Fatalf("os.MkdirAll(%s) failed unexpectedly with error: %v", insallDir, err)
}
fakeRunner := setupFakeRunner(t)
// Use invalid PID to avoid killing some process unknowingly.
plugin := &Plugin{Name: "pluginA", Revision: "revisionA", Protocol: "unix", Address: addr, EntryPath: tc.wantCmd, RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{StartAttempts: 3, StartTimeout: time.Second * 3, StartConfig: &ServiceConfig{}}, InstallPath: t.TempDir()}
if !tc.isRunning {
wantArgs = []string{fmt.Sprintf("--protocol=%s", udsProtocol), fmt.Sprintf("--address=%s", addr), fmt.Sprintf("--errorlogfile=%s", plugin.logfile())}
}
if err := connectOrReLaunch(ctx, plugin); err != nil {
t.Fatalf("connectOrReLaunch() failed unexpectedly with error: %v", err)
}
if fakeRunner.seenCommand != tc.wantCmd {
t.Errorf("launchStep.Run() executed %q, want %s ", fakeRunner.seenCommand, tc.wantCmd)
}
if diff := cmp.Diff(wantArgs, fakeRunner.seenArguments); diff != "" {
t.Errorf("launchStep.Run() executed unexpectedly with diff (-want +got):\n%s", diff)
}
})
}
}
func TestInitPluginManagerError(t *testing.T) {
ctx := context.Background()
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load(nil) failed unexpectedly with error: %v", err)
}
t.Cleanup(func() { command.CurrentMonitor().UnregisterHandler(VMEventCmd) })
if err := RegisterCmdHandler(ctx); err != nil {
t.Fatalf("RegisterCmdHandler(ctx) failed unexpectedly with error: %v", err)
}
id := "5555555555"
if _, err := InitPluginManager(ctx, id); err == nil {
t.Errorf("InitPluginManager(ctx) succeeded, want register command handler error")
}
if got := Instance().currentInstanceID(); got != id {
t.Errorf("InitPluginManager(ctx) set instance ID = %s, want %s", got, id)
}
}
func TestInitPluginManager(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
stateDir := t.TempDir()
addr := filepath.Join(t.TempDir(), "pluginA_revisionA.sock")
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\n[Core]\nacs_client = false\nsocket_connections_dir = %s", stateDir, filepath.Dir(addr))
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(nil) failed unexpectedly with error: %v", err)
}
startTestServer(t, &testPluginServer{statusFail: false, ctrs: make(map[string]int)}, udsProtocol, addr)
pluginManager.setInstanceID("1234567890")
pluginA := &Plugin{Name: "pluginA", Revision: "revisionA", Protocol: udsProtocol, Address: addr, EntryPath: "testentry/binary", RuntimeInfo: &RuntimeInfo{Pid: -5555, status: acpb.CurrentPluginStates_RUNNING}, Manifest: &Manifest{StartAttempts: 3, StartTimeout: time.Second * 3, MaxMetricDatapoints: 2, MetricsInterval: time.Second * 3}}
if err := pluginA.Store(); err != nil {
t.Fatalf("plugin.Store() failed unexpectedly with error: %v", err)
}
pluginB := &Plugin{Name: "pluginB", Revision: "revisionB", Protocol: udsProtocol, Address: "invalid-address", EntryPath: "testentry/binary", RuntimeInfo: &RuntimeInfo{Pid: -5555, status: acpb.CurrentPluginStates_CRASHED}, Manifest: &Manifest{StartAttempts: 3, StartTimeout: time.Second * 3}}
if err := pluginB.Store(); err != nil {
t.Fatalf("plugin.Store() failed unexpectedly with error: %v", err)
}
pm, err := InitPluginManager(ctx, "1234567890")
if err != nil {
t.Fatalf("InitPluginManager(ctx) failed unexpectedly with error: %v", err)
}
// InitPluginManager should have already registered the command handler.
if err := command.CurrentMonitor().RegisterHandler(VMEventCmd, nil); err == nil {
t.Errorf("RegisterHandler(%s, nil) succeeded, want error for duplicate registration attempt", VMEventCmd)
}
t.Cleanup(func() { command.CurrentMonitor().UnregisterHandler(VMEventCmd) })
tests := []struct {
name string
plugin *Plugin
wantMonitor bool
}{
{
name: "valid_plugin",
plugin: pluginA,
wantMonitor: true,
},
{
name: "invalid_plugin",
plugin: pluginB,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() { pm.stopMonitoring(tc.plugin) })
got, found := pm.plugins[tc.plugin.Name]
if !found {
t.Fatalf("InitPluginManager(ctx) failed to load plugin %q", tc.plugin.Name)
}
if got.State() != tc.plugin.State() {
t.Errorf("InitPluginManager(ctx) = state %q, want %q", got.State(), tc.plugin.State())
}
pm.pluginMonitorMu.Lock()
defer pm.pluginMonitorMu.Unlock()
if _, ok := pm.pluginMonitors[tc.plugin.FullName()]; ok != tc.wantMonitor {
t.Errorf("InitPluginManager(ctx) = added plugin monitor(%s): %t, want: %t", tc.plugin.FullName(), ok, tc.wantMonitor)
}
})
}
got := Instance()
if got != pm {
t.Errorf("Instance() = %p, want same as InitPluginManager %p", got, pm)
}
if got.protocol != udsProtocol {
t.Errorf("Instance().protocol = %s, want %s", got.protocol, udsProtocol)
}
}
func TestInitPluginManagerConfig(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
stateDir := t.TempDir()
addr := filepath.Join(t.TempDir(), "pluginA_revisionA.sock")
tests := []struct {
name string
isUDSSupported bool
connectionType string
expectedProtocol string
}{
{
name: "uds_connection_type",
isUDSSupported: true,
connectionType: udsProtocol,
expectedProtocol: udsProtocol,
},
{
name: "tcp_connection_type",
isUDSSupported: true,
connectionType: tcpProtocol,
expectedProtocol: tcpProtocol,
},
{
name: "invalid_connection_type",
isUDSSupported: true,
connectionType: "invalid",
expectedProtocol: udsProtocol,
},
{
name: "uds_not_supported",
isUDSSupported: false,
connectionType: udsProtocol,
expectedProtocol: tcpProtocol,
},
{
name: "uds_not_supported_tcp_connection_type",
isUDSSupported: false,
connectionType: tcpProtocol,
expectedProtocol: tcpProtocol,
},
{
name: "uds_not_supported_invalid_connection_type",
isUDSSupported: false,
connectionType: "invalid",
expectedProtocol: tcpProtocol,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
config := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\nconnection_type = %s\n[Core]\nacs_client = false\nsocket_connections_dir = %s", stateDir, tc.connectionType, filepath.Dir(addr))
if err := cfg.Load([]byte(config)); err != nil {
t.Fatalf("cfg.Load(nil) failed unexpectedly with error: %v", err)
}
oldIsUDSSupported := isUDSSupported
t.Cleanup(func() { isUDSSupported = oldIsUDSSupported })
isUDSSupported = func() bool { return tc.isUDSSupported }
pm, err := InitPluginManager(ctx, "1234567890")
if err != nil {
t.Fatalf("InitPluginManager(ctx) failed unexpectedly with error: %v", err)
}
if pm.protocol != tc.expectedProtocol {
t.Errorf("InitPluginManager(ctx) = protocol %q, want %q", pm.protocol, tc.expectedProtocol)
}
t.Cleanup(func() {
if err := command.CurrentMonitor().UnregisterHandler(VMEventCmd); err != nil {
t.Fatalf("command.CurrentMonitor().UnregisterHandler(VMEventCmd) failed unexpectedly with error: %v", err)
}
})
})
}
}
func TestConfigurePluginStates(t *testing.T) {
if err := cfg.Load(nil); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg.Retrieve().Core.ACSClient = false
req := &acpb.ConfigurePluginStates{
ConfigurePlugins: []*acpb.ConfigurePluginStates_ConfigurePlugin{
&acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{Name: "PluginA", RevisionId: "1"},
Manifest: &acpb.ConfigurePluginStates_Manifest{},
},
&acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{Name: "PluginA", RevisionId: "1"},
Manifest: &acpb.ConfigurePluginStates_Manifest{},
},
&acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_REMOVE,
Plugin: &acpb.ConfigurePluginStates_Plugin{Name: "PluginB", RevisionId: "2"},
Manifest: &acpb.ConfigurePluginStates_Manifest{},
},
&acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_APPLY,
Plugin: &acpb.ConfigurePluginStates_Plugin{Name: "PluginD", RevisionId: "3"},
Manifest: &acpb.ConfigurePluginStates_Manifest{Config: &acpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: "foo=bar"}},
},
&acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_ACTION_UNSPECIFIED,
},
},
}
P1 := &Plugin{Name: "PluginA", Revision: "1"}
p2 := &Plugin{Name: "PluginC", Revision: "3"}
m := map[string]*Plugin{P1.Name: P1, p2.Name: p2}
pm := &PluginManager{plugins: m, inProgressPluginRequests: make(map[string]bool), requestCount: make(map[acpb.ConfigurePluginStates_Action]map[bool]int)}
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
pm.ConfigurePluginStates(ctx, req)
// Should be a no-op.
if diff := cmp.Diff(m, pm.plugins, cmpopts.IgnoreUnexported(Plugin{})); diff != "" {
t.Errorf("ConfigurePluginStates(ctx, %+v) returned diff (-want +got):\n%s", req, diff)
}
if len(pm.inProgressPluginRequests) != 0 {
t.Errorf("ConfigurePluginStates(ctx, %+v) set pending plugins = %+v, want empty map", req, pm.inProgressPluginRequests)
}
// Without deduplication, we would have 2 install requests.
wantRequestCount := map[acpb.ConfigurePluginStates_Action]map[bool]int{
acpb.ConfigurePluginStates_INSTALL: map[bool]int{false: 1},
acpb.ConfigurePluginStates_REMOVE: map[bool]int{false: 1},
acpb.ConfigurePluginStates_APPLY: map[bool]int{false: 1},
acpb.ConfigurePluginStates_ACTION_UNSPECIFIED: map[bool]int{false: 1},
}
if diff := cmp.Diff(wantRequestCount, pm.requestCount); diff != "" {
t.Errorf("ConfigurePluginStates(ctx, %+v) returned diff (-want +got):\n%s", req, diff)
}
}
type pendingPlugins struct {
status map[string]*pendingPluginStatus
revisions map[string]bool
}
func installSetup(t *testing.T, ps *testPluginServer, addr string) (*httptest.Server, string, *testRunner, *pendingPlugins) {
t.Helper()
archive := createTestArchive(t)
hash, err := file.SHA256FileSum(archive)
if err != nil {
t.Fatalf("file.SHA256FileSum(%s) failed unexpectedly with error: %v", archive, err)
}
bytes, err := os.ReadFile(archive)
if err != nil {
t.Fatalf("os.ReadFile(%s) failed unexpectedly with error: %v", archive, err)
}
seenStates := make(map[string]*pendingPluginStatus)
seenRevisions := make(map[string]bool)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// During upgrade at this point agent should have set pending plugin
// revision. Capture it here to test that it is set correctly.
pm := Instance()
for _, plugin := range pm.plugins {
if s := plugin.pendingStatus(); s != nil {
seenStates[plugin.Name] = s
}
}
maps.Copy(seenRevisions, pm.inProgressPluginRequests)
w.Write(bytes)
}))
startTestServer(t, ps, udsProtocol, addr)
runner := setupFakeRunner(t)
return ts, hash, runner, &pendingPlugins{status: seenStates, revisions: seenRevisions}
}
func TestSetMetricConfig(t *testing.T) {
tests := []struct {
desc string
req *acpb.ConfigurePluginStates_ConfigurePlugin
want *Plugin
capacity uint
}{
{
desc: "plugin_defaults",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{},
},
want: &Plugin{
Manifest: &Manifest{
MetricsInterval: metricsCheckFrequency,
MaxMetricDatapoints: maxMetricDatapoints,
},
RuntimeInfo: &RuntimeInfo{
metrics: boundedlist.New[Metric](maxMetricDatapoints),
},
},
capacity: maxMetricDatapoints,
},
{
desc: "plugin_override_freq",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
MetricsInterval: &dpb.Duration{Seconds: 1},
},
},
want: &Plugin{
Manifest: &Manifest{
MetricsInterval: time.Second,
MaxMetricDatapoints: maxMetricDatapoints,
},
RuntimeInfo: &RuntimeInfo{
metrics: boundedlist.New[Metric](maxMetricDatapoints),
},
},
capacity: maxMetricDatapoints,
},
{
desc: "plugin_override_datapoints_count",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
MaxMetricDatapoints: 1,
},
},
want: &Plugin{
Manifest: &Manifest{
MetricsInterval: metricsCheckFrequency,
MaxMetricDatapoints: 1,
},
RuntimeInfo: &RuntimeInfo{
metrics: boundedlist.New[Metric](1),
},
},
capacity: 1,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := &Plugin{Manifest: &Manifest{}, RuntimeInfo: &RuntimeInfo{}}
got.setMetricConfig(tc.req)
if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreUnexported(Plugin{}, RuntimeInfo{}, Manifest{})); diff != "" {
t.Errorf("setMetricConfig(%+v) returned unexpected diff (-want +got):\n%s", tc.req, diff)
}
if got.RuntimeInfo.metrics.Capacity() != tc.capacity {
t.Errorf("setMetricConfig(%+v) = capacity %d, want %d", tc.req, got.RuntimeInfo.metrics.Capacity(), tc.capacity)
}
})
}
}
func TestInstallPlugin(t *testing.T) {
connections := t.TempDir()
state := t.TempDir()
setupConstraintTestClient(t)
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\nsocket_connections_dir = %s\n[Core]\nacs_client = false\n", state, connections)
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(nil) failed unexpectedly with error: %v", err)
}
addr := filepath.Join(connections, "PluginA_RevisionA.sock")
ps := &testPluginServer{ctrs: make(map[string]int)}
server, hash, runner, seenPendingPlugins := installSetup(t, ps, addr)
runner.pid = -6666
defer server.Close()
orig := pluginManager
t.Cleanup(func() { pluginManager = orig })
req := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
EntryPoint: "test-entry-point",
Checksum: hash,
},
Manifest: &acpb.ConfigurePluginStates_Manifest{
MaxMemoryUsageBytes: 1024 * 1024,
StartTimeout: &dpb.Duration{Seconds: 3},
StopTimeout: &dpb.Duration{Seconds: 5},
StartAttemptCount: 3,
DownloadAttemptCount: 2,
DownloadTimeout: &dpb.Duration{Seconds: 5},
},
}
tests := []struct {
name string
url string
installationType acpb.PluginInstallationType
wantErr bool
}{
{
name: "success_dynamic_plugin",
url: server.URL,
installationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION,
wantErr: false,
},
{
name: "success_local_plugin",
installationType: acpb.PluginInstallationType_LOCAL_INSTALLATION,
wantErr: false,
},
{
name: "failure",
installationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION,
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req.Plugin.GcsSignedUrl = tc.url
req.Manifest.Config = &acpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: tc.name}
req.Manifest.PluginInstallationType = tc.installationType
s := scheduler.Instance()
t.Cleanup(s.Stop)
pm := &PluginManager{
plugins: map[string]*Plugin{},
protocol: udsProtocol,
pluginMonitors: make(map[string]string),
pluginMetricsMonitors: make(map[string]string),
scheduler: s,
inProgressPluginRequests: make(map[string]bool),
}
pluginManager = pm
err := pm.installPlugin(ctx, req)
if (err != nil) != tc.wantErr {
t.Fatalf("installPlugin(ctx, %+v) = error: %v, want error: %t", req, err, tc.wantErr)
}
// Fresh install should not have any pending plugins.
if len(seenPendingPlugins.revisions) != 0 || len(seenPendingPlugins.status) != 0 {
t.Errorf("installPlugin(ctx, %+v) set pending plugins = %+v, want empty revision and status map", req, seenPendingPlugins)
}
if tc.wantErr {
if len(pm.plugins) != 0 {
t.Errorf("installPlugin(ctx, %+v) = %+v, want no plugin", req, pm.plugins)
}
return
}
pluginBase := pluginInstallPath(req.Plugin.Name, req.Plugin.GetRevisionId())
entryPoint := "test-entry-point"
want, err := newPlugin(req)
if err != nil {
t.Fatalf("newPlugin(%+v) failed unexpectedly with error: %v", req, err)
}
want.RuntimeInfo.Pid = runner.pid
want.Address = addr
want.Protocol = udsProtocol
if tc.installationType == acpb.PluginInstallationType_DYNAMIC_INSTALLATION {
want.InstallPath = pluginBase
entryPoint = filepath.Join(pluginBase, entryPoint)
}
want.EntryPath = entryPoint
if runner.seenCommand != entryPoint {
t.Errorf("installPlugin(ctx, %+v) executed %q, want %q", req, runner.seenCommand, entryPoint)
}
got := pm.plugins[req.Plugin.Name]
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(Plugin{}, RuntimeInfo{}, Manifest{})); diff != "" {
t.Errorf("pm.plugins[%s] returned unexpected diff (-want +got):\n%s", req.Plugin.Name, diff)
}
if got.State() != acpb.CurrentPluginStates_RUNNING {
t.Errorf("installPlugin(ctx, %+v) = plugin state %q, want %q", req, got.State(), acpb.CurrentPluginStates_RUNNING)
}
if ps.ctrs[tc.name] != 1 {
t.Errorf("installPlugin(ctx, %+v) did not call start RPC on plugin %q", req, req.Plugin.Name)
}
c := retry.Policy{MaxAttempts: 3, Jitter: time.Second * 2, BackoffFactor: 1}
err = retry.Run(ctx, c, func() error {
pm.pluginMonitorMu.Lock()
defer pm.pluginMonitorMu.Unlock()
_, ok := pm.pluginMonitors[want.FullName()]
if !ok {
return fmt.Errorf("installPlugin(ctx, %+v) did not create monitor for plugin %q", req, req.Plugin.Name)
}
return nil
})
if err != nil {
t.Errorf("%v", err)
}
})
}
}
func TestUpgradePlugin(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
connections := t.TempDir()
state := t.TempDir()
setBaseStateDir(t, state)
ctc := setupConstraintTestClient(t)
cfg.Retrieve().Plugin.SocketConnectionsDir = connections
cfg.Retrieve().Core.ACSClient = false
psClient := &mockPsClient{alive: true, exe: "test-entry-point"}
setupMockPsClient(t, psClient)
// Start plugin server - old revision.
addr1 := filepath.Join(connections, "PluginA_RevisionA.sock")
ps1 := &testPluginServer{ctrs: make(map[string]int)}
startTestServer(t, ps1, udsProtocol, addr1)
// Start plugin server - new revision.
addr := filepath.Join(connections, "PluginA_RevisionB.sock")
ps2 := &testPluginServer{ctrs: make(map[string]int)}
server, hash, runner, gotPendingPlugins := installSetup(t, ps2, addr)
defer server.Close()
orig := pluginManager
t.Cleanup(func() { pluginManager = orig })
plugin := &Plugin{Name: "PluginA", Revision: "revisionA", Protocol: udsProtocol, Address: addr1, EntryPath: "test-entry-point", RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{StartAttempts: 1, StopTimeout: time.Second * 3}}
if err := plugin.Connect(ctx); err != nil {
t.Fatalf("plugin.Connect(ctx) failed unexpectedly with error: %v", err)
}
req := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionB",
GcsSignedUrl: server.URL,
EntryPoint: "test-entry-point2",
Checksum: hash,
},
Manifest: &acpb.ConfigurePluginStates_Manifest{
MaxMemoryUsageBytes: 1024 * 1024,
MaxCpuUsagePercentage: 20,
StartTimeout: &dpb.Duration{Seconds: 3},
StartAttemptCount: 3,
DownloadAttemptCount: 2,
DownloadTimeout: &dpb.Duration{Seconds: 5},
},
}
s := scheduler.Instance()
t.Cleanup(s.Stop)
pm := &PluginManager{
plugins: map[string]*Plugin{plugin.Name: plugin},
protocol: udsProtocol,
pluginMonitors: make(map[string]string),
pluginMetricsMonitors: make(map[string]string),
scheduler: s,
inProgressPluginRequests: make(map[string]bool),
}
pluginManager = pm
if err := pm.installPlugin(ctx, req); err != nil {
t.Fatalf("installPlugin(ctx, %+v) failed unexpectedly with error: %v", req, err)
}
if !ps1.stopCalled {
t.Errorf("installPlugin(ctx, %+v) did not call stop RPC on old plugin %q", req, plugin.Name)
}
if file.Exists(addr1, file.TypeFile) {
t.Errorf("installPlugin(ctx, %+v) did not cleanup previous state, file %q still exists", req, addr1)
}
pluginBase := pluginInstallPath(req.Plugin.Name, req.Plugin.GetRevisionId())
entryPoint := filepath.Join(pluginBase, "test-entry-point2")
if runner.seenCommand != entryPoint {
t.Errorf("installPlugin(ctx, %+v) executed %q, want %q", req, runner.seenCommand, entryPoint)
}
expectedConstraint := resource.Constraint{
MaxMemoryUsage: req.GetManifest().GetMaxMemoryUsageBytes(),
MaxCPUUsage: req.GetManifest().GetMaxCpuUsagePercentage(),
}
if diff := cmp.Diff(expectedConstraint, ctc.seenConstraint, cmpopts.IgnoreFields(resource.Constraint{}, "Name")); diff != "" {
t.Errorf("installPlugin(ctx, %+v) applied unexpected constraints (-want +got):\n%s", req, diff)
}
p := pm.plugins[req.Plugin.Name]
if p.Revision != "RevisionB" {
t.Errorf("installPlugin(ctx, %+v) = %+v, did not update plugin map, want revision %s", req, p, req.Plugin.GetRevisionId())
}
// This is intermediate state we captured during the upgrade process.
wantPendingPlugins := map[string]*pendingPluginStatus{p.Name: &pendingPluginStatus{revision: req.Plugin.GetRevisionId(), status: acpb.CurrentPluginStates_INSTALLING}}
if diff := cmp.Diff(wantPendingPlugins, gotPendingPlugins.status, cmp.AllowUnexported(pendingPluginStatus{})); diff != "" {
t.Errorf("installPlugin(ctx, %+v) did not update pending plugin revisions (-want +got):\n%s", req, diff)
}
if got := plugin.pendingStatus(); got != nil {
t.Errorf("installPlugin(ctx, %+v) = pending plugin status %v, want nil on old revision %s", req, got, plugin.FullName())
}
}
func TestRemovePlugin(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
connections := t.TempDir()
state := t.TempDir()
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\n[Core]\nacs_client = false", state)
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(%s) failed unexpectedly with error: %v", tmp, err)
}
ctc := setupConstraintTestClient(t)
cfg.Retrieve().Plugin.SocketConnectionsDir = connections
addr := filepath.Join(connections, "PluginA_RevisionA.sock")
ps := &testPluginServer{ctrs: make(map[string]int)}
startTestServer(t, ps, udsProtocol, addr)
entryPoint := filepath.Join(state, "plugins", "PluginA", "test-entry-point")
createTestFile(t, entryPoint)
plugin := &Plugin{Name: "PluginA", Revision: "RevisionA", Protocol: udsProtocol, Address: addr, InstallPath: filepath.Dir(entryPoint), RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{StartAttempts: 1, StopTimeout: time.Second * 3, PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION}}
if err := plugin.Connect(ctx); err != nil {
t.Fatalf("plugin.Connect() failed unexpectedly with error: %v", err)
}
if err := os.MkdirAll(plugin.stateDir(), 0755); err != nil {
t.Fatalf("os.MkdirAll(%s) failed unexpectedly with error: %v", plugin.stateDir(), err)
}
req := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_REMOVE,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
},
}
s := scheduler.Instance()
t.Cleanup(s.Stop)
pm := &PluginManager{plugins: map[string]*Plugin{plugin.Name: plugin}, protocol: udsProtocol, pluginMonitors: make(map[string]string), scheduler: s}
if err := pm.removePlugin(ctx, req); err != nil {
t.Fatalf("removePlugin(ctx, %+v) failed unexpectedly with error: %v", req, err)
}
validatePluginRemoved(t, plugin, pm, ctc)
}
func TestMonitoring(t *testing.T) {
s := scheduler.Instance()
t.Cleanup(s.Stop)
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\n[Core]\nacs_client = false", t.TempDir())
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(%s) failed unexpectedly with error: %v", tmp, err)
}
addr := filepath.Join(t.TempDir(), "PluginA_RevisionA.sock")
cfg.Retrieve().Plugin.SocketConnectionsDir = filepath.Dir(addr)
startTestServer(t, &testPluginServer{ctrs: make(map[string]int)}, udsProtocol, "")
plugin := &Plugin{Name: "PluginA", Revision: "RevisionA", Protocol: udsProtocol, RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{StartAttempts: 1, StopTimeout: time.Second * 3}}
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
if err := plugin.Connect(ctx); err != nil {
t.Fatalf("plugin.Connect(ctx) failed unexpectedly with error: %v", err)
}
pm := &PluginManager{plugins: map[string]*Plugin{plugin.Name: plugin}, protocol: udsProtocol, pluginMonitors: make(map[string]string), scheduler: s}
pm.startMonitoring(ctx, plugin)
if _, ok := pm.pluginMonitors[plugin.FullName()]; !ok {
t.Errorf("startMonitoring(ctx, %+v) did not create and add monitor in map", plugin)
}
pm.stopMonitoring(plugin)
// Stop on non-existing plugin should be a no-op.
pm.stopMonitoring(&Plugin{Name: "non-existing", Revision: "avc"})
if _, ok := pm.pluginMonitors[plugin.FullName()]; ok {
t.Errorf("stopMonitoring(%+v) did not stop and remove monitor from map", plugin)
}
}
func TestListPluginStates(t *testing.T) {
now := time.Now()
simpleData := "simple-config"
simpleHashBytes := sha256.Sum256([]byte(simpleData))
simpleHash := hex.EncodeToString(simpleHashBytes[:])
pluginA := &Plugin{Name: "PluginA", Revision: "RevisionA", RuntimeInfo: &RuntimeInfo{health: &healthCheck{responseCode: 0, messages: []string{"ok"}, timestamp: now}, metrics: boundedlist.New[Metric](2), status: acpb.CurrentPluginStates_RUNNING}, Manifest: &Manifest{StartConfig: &ServiceConfig{Simple: simpleData}}}
pluginB := &Plugin{Name: "PluginB", Revision: "RevisionB", RuntimeInfo: &RuntimeInfo{health: &healthCheck{responseCode: 1, messages: []string{"missing pre-reqs"}, timestamp: now}, metrics: boundedlist.New[Metric](2), pendingPluginStatus: &pendingPluginStatus{revision: "RevisionB1"}, status: acpb.CurrentPluginStates_CRASHED}, Manifest: &Manifest{}}
pluginA.setPendingStatus("RevisionA1", acpb.CurrentPluginStates_INSTALLING)
pluginB.resetPendingStatus()
metric := Metric{timestamp: &tpb.Timestamp{Seconds: now.Unix()}, memoryUsage: 100, cpuUsage: 500}
pluginA.RuntimeInfo.metrics.Add(metric)
pm := &PluginManager{plugins: map[string]*Plugin{pluginA.Name: pluginA, pluginB.Name: pluginB}}
want := &acpb.CurrentPluginStates{
DaemonPluginStates: []*acpb.CurrentPluginStates_DaemonPluginState{
&acpb.CurrentPluginStates_DaemonPluginState{
Name: pluginA.Name,
CurrentRevisionId: pluginA.Revision,
CurrentPluginMetrics: []*acpb.CurrentPluginStates_Metric{
{Timestamp: metric.timestamp, MemoryUsage: metric.memoryUsage, CpuUsage: metric.cpuUsage},
},
PendingRevisionId: pluginA.RuntimeInfo.pendingPluginStatus.revision,
CurrentPluginStatus: &acpb.CurrentPluginStates_Status{
Status: pluginA.RuntimeInfo.status,
ResponseCode: pluginA.RuntimeInfo.health.responseCode,
Results: pluginA.RuntimeInfo.health.messages,
UpdateTime: tpb.New(now),
},
ConfigHash: simpleHash,
},
&acpb.CurrentPluginStates_DaemonPluginState{
Name: pluginB.Name,
CurrentRevisionId: pluginB.Revision,
CurrentPluginStatus: &acpb.CurrentPluginStates_Status{
Status: pluginB.RuntimeInfo.status,
ResponseCode: pluginB.RuntimeInfo.health.responseCode,
Results: pluginB.RuntimeInfo.health.messages,
UpdateTime: tpb.New(now),
},
},
},
}
gotResp := pm.ListPluginStates(context.Background(), &acpb.ListPluginStates{})
got := gotResp.GetDaemonPluginStates()
sort.Slice(got, func(i, j int) bool {
return got[i].GetName() < got[j].GetName()
})
if diff := cmp.Diff(want.GetDaemonPluginStates(), got, protocmp.Transform()); diff != "" {
t.Errorf("ListPluginStates(ctx, req) returned unexpected diff (-want +got):\n%s", diff)
}
// Should reset after sending metrics.
if pluginA.RuntimeInfo.metrics.Len() != 0 {
t.Errorf("ListPluginStates(ctx, req) returned unexpected number of metrics, got %d, want 1", pluginA.RuntimeInfo.metrics.Len())
}
if pluginB.RuntimeInfo.metrics.Len() != 0 {
t.Errorf("ListPluginStates(ctx, req) returned unexpected number of metrics, got %d, want 1", pluginB.RuntimeInfo.metrics.Len())
}
}
type fakeACS struct {
mu sync.Mutex
seenEvent *acpb.PluginEventMessage
seenType acpb.PluginEventMessage_PluginEventType
throwErr bool
}
func (c *fakeACS) Close() {}
func (c *fakeACS) SendMessage(msg *pb.MessageBody) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.throwErr {
return fmt.Errorf("test error")
}
m := new(acpb.PluginEventMessage)
if err := msg.GetBody().UnmarshalTo(m); err != nil {
return fmt.Errorf("Expected acmpb.AgentInfo, failed to unmarshal message body: %v", err)
}
c.seenType = m.GetEventType()
c.seenEvent = m
return nil
}
func (c *fakeACS) Receive() (*pb.MessageBody, error) {
return nil, nil
}
func TestSendEvent(t *testing.T) {
if err := cfg.Load(nil); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg.Retrieve().Core.ACSClient = true
ctx := context.Background()
plugin := &Plugin{Name: "PluginA", Revision: "RevisionA"}
tests := []struct {
desc string
shouldFail bool
}{
{
desc: "success",
},
{
desc: "failure",
shouldFail: true,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
testconnection := &fakeACS{throwErr: tc.shouldFail}
ctx = context.WithValue(ctx, client.OverrideConnection, testconnection)
sendEvent(ctx, plugin, acpb.PluginEventMessage_PLUGIN_CONFIG_INSTALL, "test-event")
if tc.shouldFail {
return
}
c := retry.Policy{MaxAttempts: 3, Jitter: time.Second * 2, BackoffFactor: 1}
err := retry.Run(ctx, c, func() error {
wantEvent := &acpb.PluginEventMessage{
PluginName: plugin.Name,
RevisionId: plugin.Revision,
EventType: acpb.PluginEventMessage_PLUGIN_CONFIG_INSTALL,
EventDetails: []byte("test-event"),
}
testconnection.mu.Lock()
gotEvent := testconnection.seenEvent
testconnection.mu.Unlock()
if diff := cmp.Diff(wantEvent, gotEvent, protocmp.Transform(), protocmp.IgnoreFields(&acpb.PluginEventMessage{}, "event_timestamp")); diff != "" {
return fmt.Errorf("sendEvent(ctx, %+v, %s, test-event) returned unexpected diff (-want +got):\n%s", plugin, acpb.PluginEventMessage_PLUGIN_CONFIG_INSTALL, diff)
}
return nil
})
if err != nil {
t.Errorf("%v", err)
}
})
}
}
func TestUpgradePluginError(t *testing.T) {
if err := cfg.Load([]byte("[Core]\nacs_client = false")); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
p1 := &Plugin{Name: "PluginA", Revision: "RevisionA", RuntimeInfo: &RuntimeInfo{}}
pm := &PluginManager{plugins: map[string]*Plugin{p1.Name: p1}, inProgressPluginRequests: map[string]bool{p1.FullName(): true}}
// Non existing plugin.
req1 := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginB",
RevisionId: "RevisionB",
},
Manifest: &acpb.ConfigurePluginStates_Manifest{},
}
// Duplicate request.
req2 := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
},
Manifest: &acpb.ConfigurePluginStates_Manifest{DownloadAttemptCount: 1},
}
// Fail pre-launch requirements, no download setup.
req3 := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA1",
},
Manifest: &acpb.ConfigurePluginStates_Manifest{
DownloadAttemptCount: 1,
},
}
tests := []struct {
desc string
req *acpb.ConfigurePluginStates_ConfigurePlugin
}{
{
desc: "non-existing-plugin",
req: req1,
},
{
desc: "duplicate-request",
req: req2,
},
{
desc: "prelaunch-fail",
req: req3,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
err := pm.upgradePlugin(context.Background(), tc.req)
if err == nil {
t.Errorf("upgradePlugin(ctx, %+v) succeeded, want error", tc.req)
}
})
}
}
func TestNewPluginManifest(t *testing.T) {
cfg := &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
"count": structpb.NewNumberValue(22),
},
}
bytes, err := proto.Marshal(cfg)
if err != nil {
t.Fatalf("proto.Marshal(%+v) failed unexpectedly with error: %v", cfg, err)
}
tests := []struct {
name string
req *acpb.ConfigurePluginStates_ConfigurePlugin
want *Manifest
}{
{
name: "simple_config",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
MaxMemoryUsageBytes: 1024 * 1024,
MaxCpuUsagePercentage: 20,
StartTimeout: &dpb.Duration{Seconds: 3},
StopTimeout: &dpb.Duration{Seconds: 3},
StartAttemptCount: 3,
Config: &acpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: "foo=bar"},
},
},
want: &Manifest{
MaxMemoryUsage: 1024 * 1024,
MaxCPUUsage: 20,
StartTimeout: time.Second * 3,
StopTimeout: time.Second * 3,
StartAttempts: 3,
StartConfig: &ServiceConfig{Simple: "foo=bar"},
},
},
{
name: "structured_config",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
StartTimeout: &dpb.Duration{Seconds: 3},
StopTimeout: &dpb.Duration{Seconds: 3},
Config: &acpb.ConfigurePluginStates_Manifest_StructConfig{StructConfig: cfg},
},
},
want: &Manifest{
StartTimeout: time.Second * 3,
StopTimeout: time.Second * 3,
StartConfig: &ServiceConfig{Structured: bytes},
},
},
{
name: "no_config",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
StartTimeout: &dpb.Duration{Seconds: 3},
StopTimeout: &dpb.Duration{Seconds: 3},
},
},
want: &Manifest{
StartTimeout: time.Second * 3,
StartConfig: &ServiceConfig{},
StopTimeout: time.Second * 3,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := newPluginManifest(tc.req)
if err != nil {
t.Fatalf("newPluginManifest(%v) returned an unexpected error: %v", tc.req, err)
}
if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreUnexported(Manifest{}), cmpopts.IgnoreFields(ServiceConfig{}, "Structured")); diff != "" {
t.Errorf("newPluginManifest(%v) returned an unexpected diff (-want +got): %v", tc.req, diff)
}
if len(tc.want.StartConfig.Structured) == 0 {
return
}
gotCfg, err := got.StartConfig.toProto()
if err != nil {
t.Fatalf("config [%+v] toProto() returned an unexpected error: %v", got.StartConfig, err)
}
if diff := cmp.Diff(cfg, gotCfg, protocmp.Transform()); diff != "" {
t.Errorf("newPluginManifest(%v) returned an unexpected diff for structured config (-want +got): %v", tc.req, diff)
}
})
}
}
func validatePluginRemoved(t *testing.T, plugin *Plugin, pm *PluginManager, ctc *testConstraintClient) {
t.Helper()
tests := []struct {
name string
path string
fType file.Type
}{
{name: "state-dir", path: plugin.stateDir(), fType: file.TypeDir},
{name: "state-file", path: plugin.stateFile(), fType: file.TypeFile},
{name: "install-dir", path: plugin.InstallPath, fType: file.TypeDir},
{name: "err-log-file", path: plugin.logfile(), fType: file.TypeFile},
{name: "socket-address-file", path: plugin.Address, fType: file.TypeFile},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := file.Exists(tc.path, tc.fType); got {
t.Errorf("Remove Plugin for %s did not remove file %q, got %t, want false", plugin.FullName(), tc.path, got)
}
})
}
if got := pm.plugins[plugin.Name]; got != nil {
t.Errorf("Remove Plugin for %s did not remove plugin from map, got %+v", plugin.FullName(), got)
}
if _, ok := pm.pluginMonitors[plugin.FullName()]; ok {
t.Errorf("Remove Plugin for %s did not remove plugin monitor from map", plugin.FullName())
}
if ctc.seenName != plugin.FullName() {
t.Errorf("Remove Plugin did not remove constraints for plugin %s, got %s", plugin.FullName(), ctc.seenName)
}
}
func TestRemoveAllDynamicPlugins(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
connections := t.TempDir()
state := t.TempDir()
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\n[Core]\nacs_client = false", state)
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(%s) failed unexpectedly with error: %v", tmp, err)
}
ctc := setupConstraintTestClient(t)
cfg.Retrieve().Plugin.SocketConnectionsDir = connections
addr := filepath.Join(connections, "PluginA_RevisionA.sock")
ps := &testPluginServer{ctrs: make(map[string]int)}
startTestServer(t, ps, udsProtocol, addr)
entryPoint := filepath.Join(state, "plugins", "PluginA", "test-entry-point")
createTestFile(t, entryPoint)
corePlugin := &Plugin{Name: "CorePlugin", Revision: "RevisionA", InstallPath: t.TempDir(), Manifest: &Manifest{PluginInstallationType: acpb.PluginInstallationType_LOCAL_INSTALLATION}}
plugin := &Plugin{Name: "PluginA", Revision: "RevisionA", Protocol: udsProtocol, Address: addr, InstallPath: filepath.Dir(entryPoint), RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{StartAttempts: 1, StopTimeout: time.Second * 3, PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION}}
if err := plugin.Connect(ctx); err != nil {
t.Fatalf("plugin.Connect() failed unexpectedly with error: %v", err)
}
if err := os.MkdirAll(plugin.stateDir(), 0755); err != nil {
t.Fatalf("os.MkdirAll(%s) failed unexpectedly with error: %v", plugin.stateDir(), err)
}
s := scheduler.Instance()
t.Cleanup(s.Stop)
pm := &PluginManager{plugins: map[string]*Plugin{plugin.Name: plugin, corePlugin.Name: corePlugin}, protocol: udsProtocol, pluginMonitors: make(map[string]string), scheduler: s}
pm.pluginMonitors[plugin.FullName()] = "test-monitor"
if err := pm.RemoveAllDynamicPlugins(ctx); err != nil {
t.Fatalf("RemoveAllDynamicPlugins(ctx) failed unexpectedly with error: %v", err)
}
validatePluginRemoved(t, plugin, pm, ctc)
// Core plugin should not be removed.
if pm.plugins[corePlugin.Name] == nil {
t.Errorf("RemoveAllDynamicPlugins(ctx) removed core plugin %s", corePlugin.Name)
}
}
func TestInitAdHocPluginManager(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
stateDir := t.TempDir()
addr := filepath.Join(t.TempDir(), "pluginA_revisionA.sock")
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\n[Core]\nacs_client = false\nsocket_connections_dir = %s", stateDir, filepath.Dir(addr))
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(nil) failed unexpectedly with error: %v", err)
}
pluginManager.setInstanceID("test-instance-id")
pluginA := &Plugin{Name: "pluginA", Revision: "revisionA", Protocol: udsProtocol, Address: addr, EntryPath: "testentry/binary", RuntimeInfo: &RuntimeInfo{Pid: -5555, status: acpb.CurrentPluginStates_RUNNING}, Manifest: &Manifest{StartAttempts: 3, StartTimeout: time.Second * 3, MaxMetricDatapoints: 2, MetricsInterval: time.Second * 3}}
if err := pluginA.Store(); err != nil {
t.Fatalf("plugin.Store() failed unexpectedly with error: %v", err)
}
pluginB := &Plugin{Name: "pluginB", Revision: "revisionB", Protocol: udsProtocol, Address: "invalid-address", EntryPath: "testentry/binary", RuntimeInfo: &RuntimeInfo{Pid: -5555, status: acpb.CurrentPluginStates_CRASHED}, Manifest: &Manifest{StartAttempts: 3, StartTimeout: time.Second * 3}}
if err := pluginB.Store(); err != nil {
t.Fatalf("plugin.Store() failed unexpectedly with error: %v", err)
}
// Reset the instance ID to test the ad-hoc plugin manager initialization.
pluginManager.setInstanceID("")
pm, err := InitAdHocPluginManager(ctx, "test-instance-id")
if err != nil {
t.Fatalf("InitAdHocPluginManager(ctx, test-instance-id) failed unexpectedly with error: %v", err)
}
if pm.instanceID != "test-instance-id" {
t.Errorf("InitAdHocPluginManager(ctx, test-instance-id) set instance ID to %q, want %q", pm.instanceID, "test-instance-id")
}
if len(pm.plugins) != 2 {
t.Errorf("InitAdHocPluginManager(ctx, test-instance-id) set %d plugins, want 2", len(pm.plugins))
}
for _, plugin := range []*Plugin{pluginA, pluginB} {
if pm.plugins[plugin.Name] == nil {
t.Errorf("InitAdHocPluginManager(ctx, test-instance-id) did not initialize plugin %s", plugin.Name)
}
}
}
func TestAdHocStopPlugin(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
connections := t.TempDir()
state := t.TempDir()
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\n[Core]\nacs_client = false", state)
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(%s) failed unexpectedly with error: %v", tmp, err)
}
ctc := setupConstraintTestClient(t)
cfg.Retrieve().Plugin.SocketConnectionsDir = connections
addr := filepath.Join(connections, "PluginA_RevisionA.sock")
ps := &testPluginServer{ctrs: make(map[string]int)}
startTestServer(t, ps, udsProtocol, addr)
entryPoint := filepath.Join(state, "plugins", "PluginA", "test-entry-point")
createTestFile(t, entryPoint)
plugin := &Plugin{Name: "PluginA", Revision: "RevisionA", Protocol: udsProtocol, Address: addr, InstallPath: filepath.Dir(entryPoint), RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{StopTimeout: time.Second * 3, PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION}}
notRunningPlugin := &Plugin{Name: "PluginB", Revision: "RevisionB", Protocol: udsProtocol, Address: t.TempDir(), RuntimeInfo: &RuntimeInfo{Pid: -6666}, Manifest: &Manifest{StopTimeout: time.Second * 3, PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION}}
pm := &PluginManager{plugins: map[string]*Plugin{plugin.Name: plugin, notRunningPlugin.Name: notRunningPlugin}, protocol: udsProtocol, pluginMonitors: make(map[string]string)}
if err := pm.StopPlugin(ctx, plugin.Name); err != nil {
t.Fatalf("StopPlugin(ctx, %s) failed unexpectedly with error: %v", plugin.Name, err)
}
validatePluginRemoved(t, plugin, pm, ctc)
for _, plugin := range []string{notRunningPlugin.Name, "non-existing-plugin"} {
if err := pm.StopPlugin(ctx, plugin); err != nil {
t.Errorf("StopPlugin(ctx, %s) failed unexpectedly with error: %v", plugin, err)
}
}
}
func TestApplyConfig(t *testing.T) {
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
stateDir := t.TempDir()
connDir := t.TempDir()
infoDir := filepath.Join(stateDir, "test-instance-id", agentStateDir, pluginInfoDir)
tmp := fmt.Sprintf("[PluginConfig]\nstate_dir = %s\nsocket_connections_dir = %s\n[Core]\nacs_client = false\n", stateDir, connDir)
if err := cfg.Load([]byte(tmp)); err != nil {
t.Fatalf("cfg.Load(nil) failed unexpectedly with error: %v", err)
}
addr := filepath.Join(connDir, "PluginA_RevisionA.sock")
ps := &testPluginServer{ctrs: make(map[string]int)}
startTestServer(t, ps, udsProtocol, addr)
setupConstraintTestClient(t)
setupMockPsClient(t, &mockPsClient{alive: true, exe: "test-entry-point"})
plugin := &Plugin{Name: "PluginA", Revision: "RevisionA", Protocol: udsProtocol, Address: addr, EntryPath: "test-entry-point", RuntimeInfo: &RuntimeInfo{Pid: -5555}, Manifest: &Manifest{startConfigHash: "oldhash", StopTimeout: time.Second * 3, StartTimeout: time.Second * 3, PluginType: acpb.PluginType_DAEMON, PluginInstallationType: acpb.PluginInstallationType_LOCAL_INSTALLATION}}
if err := plugin.Connect(ctx); err != nil {
t.Fatalf("plugin.Connect() failed unexpectedly with error: %v", err)
}
computeHash := func(data string) string {
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
pm := &PluginManager{plugins: map[string]*Plugin{plugin.Name: plugin}, protocol: udsProtocol, instanceID: "test-instance-id"}
origPluginManager := pluginManager
t.Cleanup(func() { pluginManager = origPluginManager })
pluginManager = pm
tests := []struct {
name string
plugin string
config string
wantErr bool
wantCTR int
wantHash string
wantRelaunch bool
nonExistentplugin bool
}{
{
name: "success",
plugin: "PluginA",
config: "success",
wantCTR: 1,
wantHash: computeHash("success"),
},
{
name: "success_no_config",
plugin: "PluginA",
wantCTR: 1,
wantHash: "",
},
{
name: "apply_fails",
plugin: "PluginA",
config: "failure",
wantErr: true,
wantHash: computeHash("failure"),
wantCTR: 1,
},
{
name: "unimplemented_error_relaunch",
plugin: "PluginA",
config: "unimplemented",
wantErr: false,
wantHash: computeHash("unimplemented"),
wantCTR: 2,
wantRelaunch: true,
},
{
name: "plugin_not_found",
plugin: "PluginB",
config: "nopluginfound",
wantErr: true,
wantCTR: 0,
nonExistentplugin: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tr := setupFakeRunner(t)
req := &acpb.ConfigurePluginStates_ConfigurePlugin{
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: tc.plugin,
},
Manifest: &acpb.ConfigurePluginStates_Manifest{
Config: &acpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: tc.config},
},
}
err := pm.applyConfig(ctx, req)
if (err != nil) != tc.wantErr {
t.Errorf("applyConfig(ctx, %s) = error: %v, want error: %t", tc.name, err, tc.wantErr)
}
if ps.ctrs[tc.config] != tc.wantCTR {
t.Errorf("applyConfig(ctx, %s) = %d, want %d", tc.name, ps.ctrs[tc.config], tc.wantCTR)
}
pluginMap, err := load(infoDir)
if err != nil {
t.Fatalf("load(%s) failed unexpectedly with error: %v", infoDir, err)
}
if tc.nonExistentplugin {
if p, ok := pluginMap[tc.plugin]; ok {
t.Errorf("non-existing plugin %+v was added to the plugin state after applyConfig", p)
}
} else {
if plugin.Manifest.startConfigHash != tc.wantHash {
t.Errorf("applyConfig(ctx, %s) did not reset start config hash, got %q, want %q", tc.name, plugin.Manifest.startConfigHash, tc.wantHash)
}
if got := pluginMap[tc.plugin].Manifest.StartConfig.Simple; got != tc.config {
t.Errorf("applyConfig(ctx, %s) did not update plugin state file with new config, got %q, want %q", tc.name, got, tc.config)
}
validatePluginRelaunched(t, tc.wantRelaunch, plugin, tr, ps)
}
})
}
}
func validatePluginRelaunched(t *testing.T, wantRelaunch bool, plugin *Plugin, tr *testRunner, ps *testPluginServer) {
t.Helper()
if wantRelaunch {
if tr.seenCommand != plugin.EntryPath {
t.Errorf("applyConfig for %s did not relaunch plugin with new config, got command %q, want %q", plugin.FullName(), tr.seenCommand, plugin.EntryPath)
}
if !ps.stopCalled {
t.Errorf("applyConfig for %s did not stop plugin", plugin.FullName())
}
startReq := ps.seenStartReq[plugin.Manifest.StartConfig.Simple]
if startReq == nil {
t.Errorf("applyConfig for %s did not start plugin with new config, got nil, want non-nil", plugin.FullName())
}
} else {
if tr.seenCommand != "" {
t.Errorf("applyConfig(ctx, %s) relaunched plugin unexpectedly, got command %q, want empty", plugin.FullName(), tr.seenCommand)
}
startReq := ps.seenStartReq[plugin.Manifest.StartConfig.Simple]
if startReq != nil {
t.Errorf("applyConfig for %s attempted to start plugin with new config, got %v, want nil", plugin.FullName(), startReq)
}
}
}
func TestSetConfig(t *testing.T) {
structCfg := &structpb.Struct{
Fields: map[string]*structpb.Value{
"name": structpb.NewStringValue("plugin-config"),
},
}
structBytes, err := proto.Marshal(structCfg)
if err != nil {
t.Fatalf("Failed to marshal struct config: %v", err)
}
testCases := []struct {
name string
req *acpb.ConfigurePluginStates_ConfigurePlugin
want *ServiceConfig
}{
{
name: "nil-config",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{},
},
want: &ServiceConfig{},
},
{
name: "string-config",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
Config: &acpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: "test-config"},
},
},
want: &ServiceConfig{Simple: "test-config"},
},
{
name: "struct-config",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Manifest: &acpb.ConfigurePluginStates_Manifest{
Config: &acpb.ConfigurePluginStates_Manifest_StructConfig{StructConfig: structCfg},
},
},
want: &ServiceConfig{Structured: structBytes},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manifest := &Manifest{}
err := setConfig(manifest, tc.req)
if err != nil {
t.Errorf("setConfig(%v) returned unexpected error: %v", tc.req, err)
}
if diff := cmp.Diff(tc.want, manifest.StartConfig); diff != "" {
t.Errorf("setConfig(%v) returned diff (-want +got):\n%s", tc.req, diff)
}
})
}
}
func TestGetLocalPlugin(t *testing.T) {
ctx := context.Background()
pm := &PluginManager{}
// Set the local plugin directory to the test state directory.
localDir := t.TempDir()
cfg.Load(nil)
cfg.Retrieve().Plugin.LocalPluginDir = localDir
if err := os.Mkdir(filepath.Join(localDir, "PluginA"), 0755); err != nil {
t.Fatalf("os.Mkdir(%s) failed unexpectedly with error: %v", filepath.Join(localDir, "PluginA"), err)
}
// Create a local plugin manifest file.
req := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
EntryPoint: "test-entry-point",
},
Manifest: &acpb.ConfigurePluginStates_Manifest{
MaxMemoryUsageBytes: 1024 * 1024,
StartTimeout: &dpb.Duration{Seconds: 3},
StopTimeout: &dpb.Duration{Seconds: 5},
StartAttemptCount: 3,
DownloadAttemptCount: 2,
DownloadTimeout: &dpb.Duration{Seconds: 5},
PluginInstallationType: acpb.PluginInstallationType_LOCAL_INSTALLATION,
},
}
reqBytes, err := proto.Marshal(req)
if err != nil {
t.Fatalf("proto.Marshal(%+v) failed unexpectedly with error: %v", req, err)
}
if err := os.WriteFile(filepath.Join(localDir, "PluginA", manifestFile), reqBytes, 0644); err != nil {
t.Fatalf("os.WriteFile(%s) failed unexpectedly with error: %v", filepath.Join(localDir, "PluginA", manifestFile), err)
}
got, err := pm.GetLocalPlugin(ctx, "PluginA")
if err != nil {
t.Fatalf("GetLocalPlugin(ctx, %s) failed unexpectedly with error: %v", "PluginA", err)
}
if diff := cmp.Diff(req, got, protocmp.Transform(), cmpopts.IgnoreUnexported(Plugin{}, RuntimeInfo{}, Manifest{})); diff != "" {
t.Errorf("GetLocalPlugin(ctx, %s) returned unexpected diff (-want +got):\n%s", "PluginA", diff)
}
}
func TestStartLocalPlugin(t *testing.T) {
connections := t.TempDir()
state := t.TempDir()
setBaseStateDir(t, state)
setupConstraintTestClient(t)
ctx := context.WithValue(context.Background(), client.OverrideConnection, &fakeACS{})
cfg.Retrieve().Plugin.SocketConnectionsDir = connections
cfg.Retrieve().Core.ACSClient = false
addr := filepath.Join(connections, "PluginA_RevisionA.sock")
ps := &testPluginServer{ctrs: make(map[string]int)}
server, hash, runner, seenPendingPlugins := installSetup(t, ps, addr)
runner.pid = -6666
defer server.Close()
orig := pluginManager
t.Cleanup(func() { pluginManager = orig })
tests := []struct {
name string
url string
disabled bool
onReady bool
}{
{
name: "success",
url: server.URL,
},
{
name: "disabled",
disabled: true,
},
{
name: "on-ready",
onReady: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Set the local plugin directory to the test state directory.
localDir := t.TempDir()
cfg.Retrieve().Plugin.LocalPluginDir = localDir
if err := os.Mkdir(filepath.Join(localDir, "PluginA"), 0755); err != nil {
t.Fatalf("os.Mkdir(%s) failed unexpectedly with error: %v", filepath.Join(localDir, "PluginA"), err)
}
// Create a local plugin manifest file.
req := &acpb.ConfigurePluginStates_ConfigurePlugin{
Action: acpb.ConfigurePluginStates_INSTALL,
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
EntryPoint: "test-entry-point",
Checksum: hash,
GcsSignedUrl: tc.url,
},
Manifest: &acpb.ConfigurePluginStates_Manifest{
Config: &acpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: tc.name},
MaxMemoryUsageBytes: 1024 * 1024,
StartTimeout: &dpb.Duration{Seconds: 3},
StopTimeout: &dpb.Duration{Seconds: 5},
StartAttemptCount: 3,
DownloadAttemptCount: 2,
DownloadTimeout: &dpb.Duration{Seconds: 5},
PluginInstallationType: acpb.PluginInstallationType_LOCAL_INSTALLATION,
ExecutionModel: &acpb.ExecutionModel{
BootCritical: true,
},
},
}
reqBytes, err := proto.Marshal(req)
if err != nil {
t.Fatalf("proto.Marshal(%+v) failed unexpectedly with error: %v", req, err)
}
if err := os.WriteFile(filepath.Join(localDir, "PluginA", manifestFile), reqBytes, 0644); err != nil {
t.Fatalf("os.WriteFile(%s) failed unexpectedly with error: %v", filepath.Join(localDir, "PluginA", manifestFile), err)
}
// Set up the plugin manager.
s := scheduler.Instance()
t.Cleanup(s.Stop)
pm := &PluginManager{
plugins: map[string]*Plugin{},
protocol: udsProtocol,
pluginMonitors: make(map[string]string),
pluginMetricsMonitors: make(map[string]string),
scheduler: s,
inProgressPluginRequests: make(map[string]bool),
requestCount: make(map[acpb.ConfigurePluginStates_Action]map[bool]int),
}
pluginManager = pm
var onReadyCalled bool
// Set the installations map to disable the plugin if needed, and set the
// on-ready callback if needed.
installations := make(map[string]LocalPluginInstallation)
if tc.disabled || tc.onReady {
localInstallation := LocalPluginInstallation{
Enable: !tc.disabled,
}
if tc.onReady {
localInstallation.OnReady = func(context.Context) {
onReadyCalled = true
}
}
installations["PluginA"] = localInstallation
}
// Start the local plugins for the test.
if err := pm.StartLocalPlugins(ctx, installations); err != nil {
t.Fatalf("StartLocalPlugins(ctx, %+v) = error: %v, want no error", installations, err)
}
// Fresh install should not have any pending plugins.
if len(seenPendingPlugins.revisions) != 0 || len(seenPendingPlugins.status) != 0 {
t.Errorf("StartLocalPlugins(ctx, %+v) set pending plugins = %+v, want empty revision and status map", installations, seenPendingPlugins)
}
// If the plugin is disabled or an error is expected, the plugin list should be empty.
if tc.disabled {
if len(pm.plugins) != 0 {
t.Errorf("StartLocalPlugins(ctx, %+v) = %+v, want no plugin", installations, pm.plugins)
}
return
}
if tc.onReady && !onReadyCalled {
t.Errorf("StartLocalPlugins(ctx, %+v) did not call on-ready callback", installations)
}
entryPoint := "test-entry-point"
want, err := newPlugin(req)
if err != nil {
t.Fatalf("newPlugin(%+v) failed unexpectedly with error: %v", req, err)
}
want.RuntimeInfo.Pid = runner.pid
want.Address = addr
want.Protocol = udsProtocol
want.EntryPath = entryPoint
want.Manifest.PluginInstallationType = acpb.PluginInstallationType_LOCAL_INSTALLATION
if runner.seenCommand != entryPoint {
t.Errorf("StartLocalPlugins(ctx, %+v) executed %q, want %q", installations, runner.seenCommand, entryPoint)
}
got, ok := pm.plugins[req.Plugin.Name]
if !ok {
t.Fatalf("StartLocalPlugins(ctx, %+v) did not create plugin %q", installations, req.Plugin.Name)
}
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(Plugin{}, RuntimeInfo{}, Manifest{}), cmpopts.IgnoreFields(ServiceConfig{}, "Simple"), cmpopts.IgnoreFields(Plugin{}, "InstallPath")); diff != "" {
t.Errorf("pm.plugins[%s] returned unexpected diff (-want +got):\n%s", req.Plugin.Name, diff)
}
if got.State() != acpb.CurrentPluginStates_RUNNING {
t.Errorf("StartLocalPlugins(ctx, %+v) = plugin state %q, want %q", installations, got.State(), acpb.CurrentPluginStates_RUNNING)
}
if pm.requestCount[acpb.ConfigurePluginStates_INSTALL][true] != 1 {
t.Errorf("StartLocalPlugins(ctx, %+v) called ConfigurePluginStates %d times, want 1 time", installations, pm.requestCount[acpb.ConfigurePluginStates_INSTALL][true])
}
if ps.ctrs[tc.name] != 1 {
t.Errorf("StartLocalPlugins(ctx, %+v) called start RPC %d times, want 1 time on plugin %q", installations, ps.ctrs[tc.name], req.Plugin.Name)
}
c := retry.Policy{MaxAttempts: 3, Jitter: time.Second * 2, BackoffFactor: 1}
err = retry.Run(ctx, c, func() error {
pm.pluginMonitorMu.Lock()
defer pm.pluginMonitorMu.Unlock()
_, ok := pm.pluginMonitors[want.FullName()]
if !ok {
return fmt.Errorf("StartLocalPlugins(ctx, %+v) did not create monitor for plugin %q", installations, req.Plugin.Name)
}
return nil
})
if err != nil {
t.Errorf("%v", err)
}
})
}
}
func TestVerifyPluginRunning(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
req *acpb.ConfigurePluginStates_ConfigurePlugin
isRunning bool
plugins []*Plugin
wantErr bool
}{
{
name: "success",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
EntryPoint: "test-entry-point",
},
},
plugins: []*Plugin{
&Plugin{
Name: "PluginA",
Revision: "RevisionA",
RuntimeInfo: &RuntimeInfo{
status: acpb.CurrentPluginStates_RUNNING,
},
},
},
isRunning: true,
},
{
name: "plugin-not-running",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginA",
RevisionId: "RevisionA",
EntryPoint: "test-entry-point",
},
},
plugins: []*Plugin{
&Plugin{
Name: "PluginA",
Revision: "RevisionA",
RuntimeInfo: &RuntimeInfo{
status: acpb.CurrentPluginStates_STOPPED,
},
},
},
wantErr: true,
isRunning: false,
},
{
name: "plugin-not-found",
req: &acpb.ConfigurePluginStates_ConfigurePlugin{
Plugin: &acpb.ConfigurePluginStates_Plugin{
Name: "PluginB",
RevisionId: "RevisionB",
EntryPoint: "test-entry-point",
},
},
plugins: []*Plugin{
&Plugin{
Name: "PluginA",
Revision: "RevisionA",
RuntimeInfo: &RuntimeInfo{
status: acpb.CurrentPluginStates_RUNNING,
},
},
},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
pm := &PluginManager{
plugins: map[string]*Plugin{},
}
for _, p := range tc.plugins {
pm.plugins[p.Name] = p
}
err := pm.VerifyPluginRunning(ctx, tc.req)
if (err != nil) != tc.wantErr {
t.Errorf("VerifyPluginRunning(ctx, %+v) = error: %v, want error: %t", tc.req, err, tc.wantErr)
}
})
}
}