cmd/google_guest_agent/setup/setup.go (150 lines of code) (raw):

// 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 // // http://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 setup provides the guest-agent setup functionality. package setup import ( "context" "fmt" "os" "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/acs/handler" "github.com/GoogleCloudPlatform/google-guest-agent/internal/acs/watcher" "github.com/GoogleCloudPlatform/google-guest-agent/internal/command" "github.com/GoogleCloudPlatform/google-guest-agent/internal/events" "github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata" "github.com/GoogleCloudPlatform/google-guest-agent/internal/plugin/manager" "github.com/GoogleCloudPlatform/google-guest-agent/internal/service" dpb "google.golang.org/protobuf/types/known/durationpb" ) const ( // corePluginName is the name of the core plugin. corePluginName = "GuestAgentCorePlugin" // pluginStatusRequest defines the specific status we want to check. In this case // we're checking if core plugin has completed its early initialization. pluginStatusRequest = "early-initialization" // successStatusCode is the expected status code for status request. // 0 means plugin has successfully completed initialization. successStatusCode = 0 ) // PluginManagerInterface is the minimum PluginManager interface required for // Guest Agent setup. type PluginManagerInterface interface { // ListPluginStates returns the plugin states and cached health check information. ListPluginStates(context.Context, *acpb.ListPluginStates) *acpb.CurrentPluginStates // ConfigurePluginStates configures the plugin states as stated in the request. ConfigurePluginStates(context.Context, *acpb.ConfigurePluginStates, bool) } // verifyPluginRunning verifies the plugin [name] is in running state. func verifyPluginRunning(ctx context.Context, pm PluginManagerInterface, name, revision string) error { states := pm.ListPluginStates(ctx, &acpb.ListPluginStates{}) var foundState *acpb.CurrentPluginStates_DaemonPluginState_Status for _, s := range states.GetDaemonPluginStates() { if s.GetName() == name { if s.GetCurrentPluginStatus().GetStatus() == acpb.CurrentPluginStates_DaemonPluginState_RUNNING && s.GetCurrentRevisionId() == revision { return nil } foundState = s.GetCurrentPluginStatus() } } if foundState == nil { return fmt.Errorf("core plugin %s not found, current plugins: %+v", name, states) } return fmt.Errorf("core plugin failed to start, found in state: %+v", foundState) } // install installs the core plugin and verifies if its running. func install(ctx context.Context, pm PluginManagerInterface, c Config) error { // If guest-agent is restarting and previously had installed core-plugin once // it will reconnect on [InitPluginManager]. Verify and return if running. // Requesting install again would be a no-op but will generate unnecessary // [PLUGIN_INSTALL_FAILED] event as plugin will be already present. err := verifyPluginRunning(ctx, pm, corePluginName, c.Version) if err == nil { galog.Debugf("Core plugin found in running state, skipping installation") return nil } galog.Infof("Current plugin state: %v installing core plugin...", err) req := &acpb.ConfigurePluginStates{ ConfigurePlugins: []*acpb.ConfigurePluginStates_ConfigurePlugin{ &acpb.ConfigurePluginStates_ConfigurePlugin{ Action: acpb.ConfigurePluginStates_INSTALL, Plugin: &acpb.ConfigurePluginStates_Plugin{ Name: corePluginName, RevisionId: c.Version, EntryPoint: c.CorePluginPath, }, Manifest: &acpb.ConfigurePluginStates_Manifest{ StartAttemptCount: 5, StartTimeout: &dpb.Duration{Seconds: 30}, StopTimeout: &dpb.Duration{Seconds: 30}, }, }, }, } // ConfigurePluginStates will launch the core plugin. This is blocking call // and would wait until request is completed. // Note that core plugin is already present on disk and must pass [true] // to indicate local plugin. pm.ConfigurePluginStates(ctx, req, true) // As above request is completed this check should pass/fail right away // no need to retry or wait. return verifyPluginRunning(ctx, pm, corePluginName, c.Version) } // coreReady executes components that are dependent/waiting on core plugin to be ready. func coreReady(ctx context.Context, opts Config) { galog.Debugf("Received %s ready event, setting service state to running", corePluginName) service.SetState(ctx, service.StateRunning) galog.Infof("Google Guest Agent (version: %q) Initialized...", opts.Version) } // handlePluginEvent handles the event received from plugin watcher. func handlePluginEvent(ctx context.Context, evType string, opts any, evData *events.EventData) bool { if evType != manager.EventID { galog.Debugf("Unexpected event type: %s", evType) return true } if evData.Error != nil { galog.Debugf("Still waiting for plugin status, got error: %v", evData.Error) return true } c, ok := opts.(Config) if !ok { galog.Debugf("Unexpected data type: %T, opts expected to be of type %T", opts, Config{}) return true } // Nil error means we detected the event successfully and can // run components waiting on core plugin initialization. coreReady(ctx, c) // We received the required event, no need to continue listening. return false } // Config contains options for Guest Agent setup. type Config struct { // Version is the version of the guest agent we're setting up. Version string // EnableACSWatcher determines if ACS watcher should be enabled for on-demand plugins. EnableACSWatcher bool // CorePluginPath is the path to the core plugin binary. CorePluginPath string // SkipCorePlugin determines if core plugin should be skipped. // This is used only for testing and must not be set in non-test environments. SkipCorePlugin bool } // runTimeConfig contains the runtime configuration of the instance. type runTimeConfig struct { // ID is the instance ID. id string // svcActPresent is true if the instance has service accounts attached. svcActPresent bool } func fetchRuntimeConfig(ctx context.Context, mds metadata.MDSClientInterface) (runTimeConfig, error) { // Its most likely unset and only used for testing. if got := os.Getenv("TEST_COMPUTE_INSTANCE_ID"); got != "" { return runTimeConfig{id: got, svcActPresent: true}, nil } desc, err := mds.Get(ctx) if err != nil { return runTimeConfig{}, fmt.Errorf("failed to get metadata descriptor: %w", err) } return runTimeConfig{id: desc.Instance().ID().String(), svcActPresent: desc.HasServiceAccount()}, nil } // Run orchestrates the minimum required steps for initializing Guest Agent // with core plugin. func Run(ctx context.Context, c Config) error { conf, err := fetchRuntimeConfig(ctx, metadata.New()) if err != nil { return fmt.Errorf("failed to get instance ID: %w", err) } galog.Infof("Running Guest Agent setup with config: %+v, runtime config: %+v", c, conf) // Registers the acs event watcher and initializes the acs handler if // on-demand plugins are enabled in the configuration file. // This is done as early as possible to ensure that the handler is ready // to handle to respond to non-plugin configuration requests as they serve as // heartbeat for the agent. if c.EnableACSWatcher && conf.svcActPresent { if err := events.FetchManager().AddWatcher(ctx, watcher.New()); err != nil { galog.Fatalf("Failed to add ACS watcher: %v", err) } handler.Init(c.Version) galog.Infof("Registered ACS watcher and handler") } else { galog.Infof("ACS watcher config enabled: %t, service account is present: %t, skipping ACS watcher and handler initialization. On Demand plugins will not be available.", c.EnableACSWatcher, conf.svcActPresent) } pm, err := manager.InitPluginManager(ctx, conf.id) if err != nil { return fmt.Errorf("plugin manager initialization: %w", err) } galog.Infof("Plugin manager initialized") go func() { if err := command.Setup(ctx, command.ListenerGuestAgent); err != nil { galog.Errorf("Failed to setup command monitor for Guest Agent: %v", err) } }() // If core plugin initialization is skipped just assume instance is ready // and run as if core-plugin has already sent ready event. if c.SkipCorePlugin { galog.Debug("Skipping core plugin initialization") coreReady(ctx, c) return nil } if err := install(ctx, pm, c); err != nil { return fmt.Errorf("core plugin installation: %w", err) } events.FetchManager().Subscribe(manager.EventID, events.EventSubscriber{Name: "GuestAgent", Data: c, Callback: handlePluginEvent}) // Ignore returned [watcher] as it takes care of deregistering itself. _, err = manager.InitWatcher(ctx, corePluginName, successStatusCode, pluginStatusRequest) if err != nil { return fmt.Errorf("init %s watcher: %w", corePluginName, err) } return nil }