cmd/core_plugin/core_plugin.go (113 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 main is the implementation of the guest agent's core plugin.
package main
import (
"flag"
"fmt"
"os"
"strings"
"sync/atomic"
"text/template"
"time"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/manager"
"github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/stages/early"
"github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/stages/late"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/logger"
)
var (
// logOpts holds the logger options. It's mapped to command line flags.
logOpts = logger.Options{
// Core plugin uses the same "local" ident as the guest agent. For example,
// With linux's syslog both core plugin and guest-agent will be stored under
// the same "name space".
Ident: logger.LocalLoggerIdent,
// Since core plugin and guest agent are using the same ident, use a prefix
// to differentiate their entries in the logs.
Prefix: logger.CorePluginLogPrefix,
// CloudIdent is the cloud logging's logId attribute - or logName field
// visible to the user.
CloudIdent: logger.CloudLoggingLogID,
}
// version is the version of the binary.
version = "unknown"
// protocol is the protocol to use tcp/uds.
protocol string
// address is the address to start server listening on.
address string
// listModules is a flag that forces the plugin to list the modules and exit.
listModules bool
// errorLogFile is the path to the error log file.
errorLogFile string
// loggerInitialized is a flag that indicates if the logger has been
// initialized. This is used to make sure we don't log anything before the
// logger is initialized.
loggerInitialized atomic.Bool
)
const (
// galogShutdownTimeout is the period of time we should wait galog to
// shutdown.
galogShutdownTimeout = time.Second
)
func setupFlags() {
enableCloudLogging := true
// In test environments we don't have access to the cloud logging API, in such
// a scenario we inject CORE_PLUGIN_CLOUD_LOGGING_ENABLED=false to disable
// core-plugin cloud logging support avoiding tests hanging. In some
// environments we don't have direct access to core-plugin's cli directly
// hence the environment variable.
if val := os.Getenv("CORE_PLUGIN_CLOUD_LOGGING_ENABLED"); val != "" {
val = strings.ToLower(val)
for _, v := range []string{"0", "false", "no", "off"} {
if val == v {
enableCloudLogging = false
break
}
}
}
// Log flags. When running in a plugin context these flags will be propagated
// by the guest agent's plugin manager.
flag.StringVar(&logOpts.LogFile, "logfile", cfg.Retrieve().Core.LogFile, "path to the log file")
flag.BoolVar(&logOpts.LogToStderr, "logtostderr", false, "write logs to stderr")
flag.BoolVar(&logOpts.LogToCloudLogging, "logtocloud", enableCloudLogging, "write logs to cloud logging")
flag.IntVar(&logOpts.Level, "loglevel", cfg.Retrieve().Core.LogLevel, "log level: "+galog.ValidLevels())
flag.IntVar(&logOpts.Verbosity, "logverbosity", cfg.Retrieve().Core.LogVerbosity, "log verbosity")
flag.StringVar(&protocol, "protocol", "", "protocol to use uds/tcp")
flag.StringVar(&address, "address", "", "address to start server listening on")
flag.BoolVar(&listModules, "listmodules", false, "list available modules and exit")
flag.StringVar(&errorLogFile, "errorlogfile", "", "path to the fatal error log file")
// Ident is propagated by the guest agent's plugin manager so all the logs can
// be consolidated in the same syslog/event log bucket.
flag.StringVar(&logOpts.Ident, "ident", logOpts.Ident, "ident used to record local system log entries")
flag.Parse()
}
func main() {
// Loads the default config definitions and merges them with the user defined
// ones.
if err := cfg.Load(nil); err != nil {
logAndExit(fmt.Sprintf("Failed to load config: %v", err))
}
// Set the version of the binary as soon as config is loaded for any other
// modules to use. Setting value explicitly after cfg load makes sure version
// is as expected and its not coming from instance config or any other files.
cfg.Retrieve().Core.Version = version
// Setup flag pointers and parse the provided values. setupFlags() depends on
// having the cfg package initialized as it depends on some default values
// coming from user's configuration.
setupFlags()
// List available modules and exit.
if listModules {
exitCode, err := displayModules()
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to list modules:", err)
}
os.Exit(exitCode)
}
// Start the plugin server, the plugin actual entry point is implemented on
// behalf of the Start() operation. The plugin is not considered started until
// a "start" rpc call is issued, the application "main" context is defined by
// plugin's Start() operation.
if err := initPluginServer(); err != nil {
logAndExit(fmt.Sprintf("Failed to start plugin server: %v", err))
}
}
// displayModules displays the list of available modules.
func displayModules() (int, error) {
const tmpl = `List of currently available and enabled modules:
Early stage modules:
{{range .EarlyModules}} + {{.Display}}
{{end}}
Late stage modules:
{{range .LateModules}} + {{.Display}}
{{end}}`
data := struct {
EarlyModules []*manager.Module
LateModules []*manager.Module
}{
early.Retrieve().ListModules(),
late.Retrieve().ListModules(),
}
t, err := template.New("").Parse(tmpl)
if err != nil {
return 1, fmt.Errorf("failed to parse modules list template: %w", err)
}
buffer := new(strings.Builder)
err = t.Execute(buffer, data)
if err != nil {
return 1, fmt.Errorf("failed to execute modules list template: %w", err)
}
fmt.Println(buffer.String())
return 0, nil
}
// logAndExit logs the message and exits the core-plugin. This is a helper
// function which also writes the message to the error log file which is
// captured by the guest agent and sent to the ACS. This is not using galog as
// we want to capture only fatal errors and not every log message here.
// Also, it doesn't support file rotation yet which mean file will grow
// indefinitely. Galog file logger will be used only if user has enabled it.
func logAndExit(msg string) {
var err error
if errorLogFile != "" {
// Ignore the error if logger is not initialized. As we are exiting anyways
// and there's nothing we can do about the error.
err = os.WriteFile(errorLogFile, []byte(msg), 0644)
}
if loggerInitialized.Load() {
if err != nil {
galog.Errorf("Failed to write error log file %q: %v", errorLogFile, err)
}
galog.Fatal(msg)
}
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
}