internal/commandProcessor/commandProcessor.go (175 lines of code) (raw):
package commandProcessor
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/Azure/azure-extension-platform/pkg/logging"
"github.com/Azure/run-command-handler-linux/internal/constants"
"github.com/Azure/run-command-handler-linux/internal/handlersettings"
"github.com/Azure/run-command-handler-linux/internal/instanceview"
"github.com/Azure/run-command-handler-linux/internal/types"
"github.com/Azure/run-command-handler-linux/pkg/seqnumutil"
"github.com/Azure/run-command-handler-linux/pkg/versionutil"
"github.com/go-kit/kit/log"
"github.com/pkg/errors"
)
func ProcessImmediateHandlerCommand(cmd types.Cmd, hs handlersettings.HandlerSettingsFile, extensionName string, seqNum int) error {
ctx := initializeLogger(cmd)
ctx = ctx.With("extensionName", extensionName)
ctx.Log("event", "start")
ctx.Log("message", "processing immediate command")
hEnv, err := getHandlerEnv(ctx)
if err != nil {
return errors.Wrap(err, "could not get handler environment")
}
err = executePreSteps(ctx, cmd, hEnv, extensionName, seqNum, constants.ImmediateDownloadFolder)
if err != nil {
return errors.Wrap(err, "failed on pre steps")
}
err = storeConfigSettingsFileForLocalExecution(ctx, hs, hEnv, extensionName, seqNum)
if err != nil {
return errors.Wrap(err, "failed when trying to store handler settings locally")
}
// Store handler settings locally before moving forward...
return ProcessHandlerCommandWithDetails(ctx, cmd, hEnv, extensionName, seqNum, constants.ImmediateDownloadFolder)
}
func ProcessHandlerCommand(cmd types.Cmd) error {
ctx := initializeLogger(cmd)
ctx.Log("event", "start")
ctx.Log("message", "processing command")
hEnv, extensionName, seqNum, err := getRequiredInitialVariables(ctx)
if err != nil {
return errors.Wrap(err, "could not get initial required variables")
}
ctx = ctx.With("extensionName", extensionName)
err = executePreSteps(ctx, cmd, hEnv, extensionName, seqNum, constants.DownloadFolder)
if err != nil {
return errors.Wrap(err, "failed on pre steps")
}
return ProcessHandlerCommandWithDetails(ctx, cmd, hEnv, extensionName, seqNum, constants.DownloadFolder)
}
func ProcessHandlerCommandWithDetails(ctx *log.Context, cmd types.Cmd, hEnv types.HandlerEnvironment, extensionName string, seqNum int, downloadFolder string) error {
ctx.Log("message", fmt.Sprintf("processing command for extensionName: %v and seqNum: %v", extensionName, seqNum))
instView := types.RunCommandInstanceView{
ExecutionState: types.Running,
ExecutionMessage: "Execution in progress",
ExitCode: 0,
Output: "",
Error: "",
StartTime: time.Now().UTC().Format(time.RFC3339),
EndTime: "",
}
metadata := types.NewRCMetadata(extensionName, seqNum, downloadFolder, constants.DataDir)
instanceview.ReportInstanceView(ctx, hEnv, metadata, types.StatusTransitioning, cmd, &instView)
// execute the subcommand
stdout, stderr, cmdInvokeError, exitCode := cmd.Functions.Invoke(ctx, hEnv, &instView, metadata, cmd)
instView.Output = stdout
instView.Error = stderr
if cmdInvokeError != nil {
ctx.Log("event", "failed to handle", "error", cmdInvokeError)
instView.ExecutionMessage = "Execution failed: " + cmdInvokeError.Error()
instView.ExecutionState = types.Failed
instView.EndTime = time.Now().UTC().Format(time.RFC3339)
instView.ExitCode = exitCode
statusToReport := types.StatusSuccess
// If TreatFailureAsDeploymentFailure is set to true and the exit code is non-zero, set extension status to error
cfg, err := handlersettings.GetHandlerSettings(hEnv.HandlerEnvironment.ConfigFolder, extensionName, seqNum, ctx)
if err == nil && cfg.PublicSettings.TreatFailureAsDeploymentFailure && cmd.FailExitCode != 0 {
statusToReport = types.StatusError
}
instanceview.ReportInstanceView(ctx, hEnv, metadata, statusToReport, cmd, &instView)
return errors.Wrapf(err, "command execution failed")
} else { // No error. Succeeded
instView.ExecutionMessage = "Execution completed"
instView.ExecutionState = types.Succeeded
instView.EndTime = time.Now().UTC().Format(time.RFC3339)
instView.ExitCode = constants.ExitCode_Okay
}
instanceview.ReportInstanceView(ctx, hEnv, metadata, types.StatusSuccess, cmd, &instView)
ctx.Log("event", "end")
return nil
}
func getRequiredInitialVariables(ctx *log.Context) (types.HandlerEnvironment, string, int, error) {
var seqNum int
var extensionName string
ctx.Log("message", "getting required initial variables")
hEnv, err := getHandlerEnv(ctx)
if err != nil {
return hEnv, extensionName, seqNum, errors.Wrap(err, "failed to parse handlerEnv")
}
extensionName = getExtensionName(ctx)
seqNum, err = getSeqNum(&ctx, hEnv, extensionName)
if err != nil {
return hEnv, extensionName, seqNum, errors.Wrap(err, "failed to get seqNum")
}
return hEnv, extensionName, seqNum, nil
}
func executePreSteps(ctx *log.Context, cmd types.Cmd, hEnv types.HandlerEnvironment, extensionName string, seqNum int, downloadFolder string) error {
// check sub-command preconditions, if any, before executing
if cmd.Functions.Pre != nil {
ctx.Log("event", "pre-check")
metadata := types.NewRCMetadata(extensionName, seqNum, downloadFolder, constants.DataDir)
if err := cmd.Functions.Pre(ctx, hEnv, metadata, cmd); err != nil {
ctx.Log("event", "pre-check failed", "error", err)
return errors.Wrapf(err, "pre-check step failed")
}
}
return nil
}
func initializeLogger(cmd types.Cmd) *log.Context {
logging.New(nil)
ctx := log.NewContext(log.NewSyncLogger(log.NewLogfmtLogger(
os.Stdout))).With("time", log.DefaultTimestamp).With("version", versionutil.VersionString())
ctx = ctx.With("operation", strings.ToLower(cmd.Name))
return ctx
}
func getHandlerEnv(ctx *log.Context) (types.HandlerEnvironment, error) {
// parse extension handler environment
hEnv, err := handlersettings.GetHandlerEnv()
if err != nil {
ctx.Log("message", "failed to parse handlerEnv", "error", err)
return hEnv, err
}
return hEnv, nil
}
func getExtensionName(ctx *log.Context) string {
extensionName := os.Getenv(constants.ConfigExtensionNameEnvName)
ctx.Log("extensionName", extensionName)
return extensionName
}
func getSeqNum(ctx **log.Context, hEnv types.HandlerEnvironment, extensionName string) (int, error) {
// Multiconfig support: Agent should set env variables for the extension name and sequence number
seqNum := -1
var err error = nil
seqNumVariable := os.Getenv(constants.ConfigSequenceNumberEnvName)
if seqNumVariable != "" {
seqNum, err = strconv.Atoi(seqNumVariable)
if err != nil {
msg := fmt.Sprintf("failed to parse env variable ConfigSequenceNumber: %v", seqNumVariable)
(*ctx).Log("message", msg, "error", err)
return seqNum, errors.Wrap(err, msg)
}
}
// Read the seqNum from latest config file in case VMAgent did not set it as env variable
if seqNum == -1 {
seqNum, err = seqnumutil.FindSequenceNumberFromConfig(hEnv.HandlerEnvironment.ConfigFolder, constants.ConfigFileExtension, extensionName)
if err != nil {
(*ctx).Log("FindSequenceNumberFromConfig", "failed to find sequence number from config folder.", "error", err)
} else {
(*ctx).Log("FindSequenceNumberFromConfig", fmt.Sprintf("Sequence number determined from config folder: %d", seqNum))
}
}
(*ctx) = (*ctx).With("seq", seqNum)
(*ctx).Log("seqNum", seqNum)
return seqNum, nil
}
func storeConfigSettingsFileForLocalExecution(ctx *log.Context, hs handlersettings.HandlerSettingsFile, hEnv types.HandlerEnvironment, extensionName string, seqNum int) error {
configFolder := hEnv.HandlerEnvironment.ConfigFolder
configFilePath := handlersettings.GetConfigFilePath(configFolder, seqNum, extensionName)
ctx.Log("message", fmt.Sprintf("saving handler settings in file: %v", configFilePath))
content, err := json.Marshal(hs)
if err != nil {
return errors.Wrap(err, "could not marshal handler settings file")
}
err = os.WriteFile(configFilePath, content, 0644)
if err != nil {
return errors.Wrap(err, "could not store handler settings file locally to run the command")
}
return nil
}