blob: e300bf4efa1cf27505ad44f6a78d90a4c21f27fb [file] [edit]
// Copyright 2025 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"
"fmt"
"os"
"path/filepath"
"testing"
acpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)
// cleanupTestPlugin is a test plugin for cleanup.
type cleanupTestPlugin struct {
name, revision string
}
// String returns the full name of the plugin.
func (c *cleanupTestPlugin) String() string {
return fmt.Sprintf("%s_%s", c.name, c.revision)
}
func TestRetryFailedRemovals(t *testing.T) {
state := t.TempDir()
setBaseStateDir(t, state)
pluginDir := filepath.Join(state, "plugins")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatalf("os.MkdirAll(%s) failed unexpectedly with error: %v", pluginDir, err)
}
// Make some plugins.
failedPlugin := cleanupTestPlugin{name: "plugin1", revision: "revision1"}
uncachedPlugin := cleanupTestPlugin{name: "plugin2", revision: "revision2"}
runningPlugin := cleanupTestPlugin{name: "plugin3", revision: "revision3"}
// Create a plugin directory for each plugin.
if err := os.Mkdir(filepath.Join(pluginDir, failedPlugin.String()), 0755); err != nil {
t.Fatalf("os.Mkdir(%s) failed unexpectedly with error: %v", filepath.Join(pluginDir, failedPlugin.String()), err)
}
if err := os.Mkdir(filepath.Join(pluginDir, uncachedPlugin.String()), 0755); err != nil {
t.Fatalf("os.Mkdir(%s) failed unexpectedly with error: %v", filepath.Join(pluginDir, uncachedPlugin.String()), err)
}
if err := os.Mkdir(filepath.Join(pluginDir, runningPlugin.String()), 0755); err != nil {
t.Fatalf("os.Mkdir(%s) failed unexpectedly with error: %v", filepath.Join(pluginDir, runningPlugin.String()), err)
}
// Create links to the plugin directories.
if err := os.Symlink(filepath.Join(pluginDir, failedPlugin.String()), filepath.Join(pluginDir, failedPlugin.name)); err != nil {
t.Fatalf("os.Symlink(%s, %s) failed unexpectedly with error: %v", filepath.Join(pluginDir, failedPlugin.String()), filepath.Join(pluginDir, failedPlugin.name), err)
}
if err := os.Symlink(filepath.Join(pluginDir, uncachedPlugin.String()), filepath.Join(pluginDir, uncachedPlugin.name)); err != nil {
t.Fatalf("os.Symlink(%s, %s) failed unexpectedly with error: %v", filepath.Join(pluginDir, uncachedPlugin.String()), filepath.Join(pluginDir, uncachedPlugin.name), err)
}
if err := os.Symlink(filepath.Join(pluginDir, runningPlugin.String()), filepath.Join(pluginDir, runningPlugin.name)); err != nil {
t.Fatalf("os.Symlink(%s, %s) failed unexpectedly with error: %v", filepath.Join(pluginDir, runningPlugin.String()), filepath.Join(pluginDir, runningPlugin.name), err)
}
// Initialize the plugin manager with the test plugins. Only the running
// plugin is cached in the plugin manager.
plugins := make(map[string]*Plugin)
for _, p := range []cleanupTestPlugin{runningPlugin} {
plugins[p.String()] = &Plugin{Name: p.name, Revision: p.revision, InstallPath: filepath.Join(pluginDir, p.String()), Manifest: &Manifest{PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION}}
}
pm := &PluginManager{instanceID: "1234567890", plugins: plugins}
// Retry failed removals.
if _, err := pm.retryFailedRemovals(context.Background()); err != nil {
t.Fatalf("retryFailedRemovals(ctx) failed unexpectedly with error: %v", err)
}
tests := []struct {
name string
plugin cleanupTestPlugin
exists bool
}{
{
name: "failed-plugin-retried",
plugin: failedPlugin,
exists: false,
},
{
name: "uncached-plugin-removed",
plugin: uncachedPlugin,
exists: false,
},
{
name: "running-plugin-unchanged",
plugin: runningPlugin,
exists: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dir := filepath.Join(pluginDir, tc.plugin.String())
if got := file.Exists(dir, file.TypeDir); got != tc.exists {
t.Errorf("retryFailedRemovals(ctx): dir %q exists: %t, should exist: %t", dir, got, tc.exists)
}
link := filepath.Join(pluginDir, tc.plugin.name)
if _, err := os.Stat(link); (err == nil) != tc.exists {
t.Errorf("retryFailedRemovals(ctx): link %q exists: %t, should exist: %t", link, err == nil, tc.exists)
}
})
}
}
func TestCleanupOldState(t *testing.T) {
state := t.TempDir()
oldInstance := filepath.Join(state, "1234567890")
newInstance := filepath.Join(state, "9876543210")
nonNumericDir := filepath.Join(state, "non-numeric-dir")
alphaNumericDir := filepath.Join(state, "abc-1234567890")
otherFile := filepath.Join(state, "random-file")
f, err := os.Create(otherFile)
if err != nil {
t.Fatalf("os.Create(%s) failed unexpectedly with error: %v", filepath.Join(state, "random-file"), err)
}
if err := f.Close(); err != nil {
t.Fatalf("f.Close(%s) failed unexpectedly with error: %v", otherFile, err)
}
for _, d := range []string{oldInstance, newInstance, nonNumericDir, alphaNumericDir} {
if err := os.MkdirAll(d, 0755); err != nil {
t.Fatalf("os.MkdirAll(%s) failed unexpectedly with error: %v", d, err)
}
}
pm := &PluginManager{instanceID: filepath.Base(newInstance)}
if err := pm.cleanupOldState(context.Background(), state); err != nil {
t.Fatalf("cleanupOldState(ctx, %s) failed unexpectedly with error: %v", state, err)
}
tests := []struct {
name string
path string
fType file.Type
exists bool
}{
{
name: "old-instance-cleanup",
path: oldInstance,
exists: false,
fType: file.TypeDir,
},
{
name: "new-instance-unchanged",
path: newInstance,
exists: true,
fType: file.TypeDir,
},
{
name: "non-numeric-dir-unchanged",
path: nonNumericDir,
exists: true,
fType: file.TypeDir,
},
{
name: "alpha-numeric-dir-unchanged",
path: alphaNumericDir,
exists: true,
fType: file.TypeDir,
},
{
name: "non-dir-file-unchanged",
path: otherFile,
exists: true,
fType: file.TypeFile,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := file.Exists(tc.path, tc.fType); got != tc.exists {
t.Errorf("cleanupOldState(ctx, %s) ran, file %q exists: %t, should exist: %t", state, tc.path, got, tc.exists)
}
})
}
nonExistingDir := filepath.Join(state, "non-existing-dir")
if err := pm.cleanupOldState(context.Background(), nonExistingDir); err != nil {
t.Errorf("cleanupOldState(ctx, %s) failed unexpectedly with error: %v, want nil for non-existing directory", nonExistingDir, err)
}
}