| // 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) |
| } |
| } |