blob: 35e0536bc1853947aec160e9b158c9fb5b3abd57 [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"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/GoogleCloudPlatform/galog"
acpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)
// retryFailedRemovals removes all old plugin states.
//
// This function is called by the cleanup watcher to remove old plugin states.
// This will primarily retry removing plugins that weren't properly removed before.
func (m *PluginManager) retryFailedRemovals(ctx context.Context) (bool, error) {
galog.Debugf("Cleaning up old/failed plugin states")
// Check for plugins that aren't cached by the guest agent that should be removed.
pluginInstallLoc := filepath.Join(baseState(), pluginInstallDir)
pluginDirs, err := os.ReadDir(pluginInstallLoc)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
galog.V(1).Debugf("Plugin install directory %q does not exist, nothing to clean up", pluginInstallLoc)
return true, nil
}
return true, fmt.Errorf("failed to read plugin install directory: %w", err)
}
// Keep track of the links to the plugin directories. We need to verify that
// the link points to a plugin directory that is no longer cached by the guest
// agent before removing it.
var links []string
failedRemovals := make(map[string]*Plugin)
m.pendingPluginRevisionsMu.RLock()
for _, p := range pluginDirs {
if !p.IsDir() {
links = append(links, p.Name())
continue
}
// Plugin directories have the form <name>_<revision>. The revision is
// typically a hash, so it should be safe to split by the last underscore.
nameIndex := strings.LastIndex(p.Name(), "_")
pluginName := p.Name()[:nameIndex]
pluginRevision := p.Name()[nameIndex+1:]
galog.V(1).Debugf("Checking plugin %q, revision %q", pluginName, pluginRevision)
// We want to avoid cleaning up plugins that currently have a request in progress.
// This is to prevent a race condition where the plugin is removed in the
// middle of an installation request.
if m.inProgressPluginRequests[pluginName] {
continue
}
if _, err := m.Fetch(p.Name()); err != nil {
installPath := filepath.Join(pluginInstallLoc, p.Name())
failedRemovals[p.Name()] = &Plugin{Name: pluginName, Revision: pluginRevision, InstallPath: installPath, Manifest: &Manifest{PluginInstallationType: acpb.PluginInstallationType_DYNAMIC_INSTALLATION}}
galog.Debugf("Found leftover plugin %q, marking for removal", p.Name())
}
}
m.pendingPluginRevisionsMu.RUnlock()
// Remove the links to the plugin directories that are no longer cached by the guest agent.
for _, link := range links {
dest, err := os.Readlink(filepath.Join(pluginInstallLoc, link))
if err != nil {
continue
}
// Check if the link points to a plugin directory that is no longer cached
// by the guest agent.
galog.V(1).Debugf("Checking link %q", link)
if _, ok := failedRemovals[filepath.Base(dest)]; ok {
linkPath := filepath.Join(pluginInstallLoc, link)
galog.V(1).Debugf("Removing link %q", linkPath)
if err := os.Remove(linkPath); err != nil {
galog.Debugf("Unable to remove link %q: %v", linkPath, err)
}
}
}
var errs []error
noop := len(failedRemovals) == 0
for _, p := range failedRemovals {
galog.Debugf("Cleaning up plugin %q", p.FullName())
if err := cleanup(ctx, p); err != nil {
errs = append(errs, err)
}
}
galog.Debugf("Finished cleaning up old/failed plugin states")
return noop, errors.Join(errs...)
}
func (m *PluginManager) cleanupOldState(ctx context.Context, path string) error {
re := regexp.MustCompile("^[0-9]+$")
if !file.Exists(path, file.TypeDir) {
// This is not an error, it just means there's nothing to clean up, which
// can happen if the agent is started for the first time or plugins were
// never installed.
galog.Debugf("Plugin state directory %q does not exist, skipping cleanup", path)
return nil
}
dirs, err := os.ReadDir(path)
if err != nil {
return fmt.Errorf("failed to read directory %q: %w", path, err)
}
currentID := m.currentInstanceID()
for _, dir := range dirs {
absPath := filepath.Join(path, dir.Name())
// Skip the current instance directory and any non-numeric directories,
// these are most likely not agent created.
if !dir.IsDir() || dir.Name() == currentID || !re.MatchString(dir.Name()) {
galog.V(2).Debugf("Skipping %q from plugin manager old state cleanup", absPath)
continue
}
galog.Debugf("Removing previous plugin state %q", absPath)
if err := os.RemoveAll(absPath); err != nil {
return fmt.Errorf("failed to remove file %q: %w", absPath, err)
}
}
return nil
}
// CleanupJob is a job that cleans up old plugin states.
type CleanupJob struct {
pm *PluginManager
}
// newCleanupJob creates a new cleanup job.
func newCleanupJob(pm *PluginManager) *CleanupJob {
return &CleanupJob{pm: pm}
}
// ID returns the ID of the cleanup job.
func (s *CleanupJob) ID() string {
return "cleanup"
}
// MetricName returns the metric name of the cleanup job.
func (s *CleanupJob) MetricName() acpb.GuestAgentModuleMetric_Metric {
return acpb.GuestAgentModuleMetric_PLUGIN_CLEANUP
}
// Interval returns the interval of the cleanup job. This is set to 24 hours
// to run once a day.
func (s *CleanupJob) Interval() (time.Duration, bool) {
return 24 * time.Hour, true
}
// ShouldEnable returns true if the cleanup job should be enabled.
//
// This should always be enabled to ensure that old plugin states are cleaned
// up.
func (s *CleanupJob) ShouldEnable(context.Context) bool {
return true
}
// Run runs the cleanup job.
func (s *CleanupJob) Run(ctx context.Context) (bool, error) {
galog.Debugf("Running cleanup job")
noop, err := s.pm.retryFailedRemovals(ctx)
galog.Debugf("Finished running cleanup job")
return noop, err
}