Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2dfe059
json serialization for errorclarification
Jun 11, 2025
fab4362
Error clarification enum added to constants
Jun 12, 2025
1ab7cd2
status changes
Jun 20, 2025
480a52a
Logic for reprting errorclassifcation
Jun 30, 2025
6321c4b
dependency resolution
Jun 30, 2025
506e436
dependency resolution
Jun 30, 2025
bb45ec4
chore: tidy go modules
Jun 30, 2025
cc86515
Error clarification mapping
Jul 1, 2025
f2eb12a
test hardcode of substatus
Jul 7, 2025
b23a31d
remove hardcode
Jul 7, 2025
e614d8e
test substatus
Jul 7, 2025
5632d14
test
Jul 7, 2025
edabf77
test
Jul 7, 2025
c3d4722
test
Jul 7, 2025
18f7ef1
test status
Jul 7, 2025
097ee8a
uodated substatus to pass verification
Jul 7, 2025
90e75db
unit tests and fixes
Jul 22, 2025
51bcc55
test reflect
Jul 28, 2025
48a5c95
fixing reflect func
Jul 28, 2025
1a782c2
comment resolution
Jul 28, 2025
d586edb
typo
Jul 28, 2025
6c028e6
test functionality
Jul 28, 2025
cbc7bed
test new status
Jul 28, 2025
7668b1e
Merge pull request #1 from FeyiAdeaga/testError
FeyiAdeaga Jul 28, 2025
85d314d
remove redundant error clarification
Jul 29, 2025
3c8ea51
spelling error
Aug 5, 2025
1ceb207
Merge branch 'dev/t-feadeaga_error_changes' into t-feyiadeaga_error_c…
FeyiAdeaga Aug 8, 2025
3c5c8c0
Merge pull request #55 from FeyiAdeaga/t-feyiadeaga_error_clarificati…
FeyiAdeaga Aug 8, 2025
ac67e9d
Checkpoint - updating RCV2 for user errors
jscalev Aug 29, 2025
602c525
exe changes
jscalev Sep 23, 2025
76c8e6b
Merge branch 'main' into dev/t-feadeaga_error_changes
jscalev Oct 13, 2025
164f058
Update
jscalev Oct 13, 2025
df614a5
Another checkpoint
jscalev Nov 26, 2025
92d9f1d
Checkpoint
jscalev Dec 23, 2025
cc5e8a4
Adds tests
jscalev Dec 24, 2025
e771d11
More unit tests
jscalev Dec 30, 2025
fc68763
Adds more unit tests
jscalev Dec 31, 2025
925dae8
Adds more unit tests
jscalev Dec 31, 2025
795a373
Fixes build break
jscalev Jan 6, 2026
78f910b
Switches to new library methods
jscalev Jan 7, 2026
1469f2d
Fixes unit tests
jscalev Jan 8, 2026
fac6880
More unit test fixes
jscalev Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.24.5

require (
github.com/Azure/azure-extension-foundation v0.0.0-20250620154556-caff9e3c3c5c
github.com/Azure/azure-extension-platform v0.0.0-20250107200156-aa20f765d49f
github.com/Azure/azure-extension-platform v0.0.0-20260107210613-2a62cc200c34
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/Azure/azure-extension-platform v0.0.0-20240610175536-404c704f82f8 h1:
github.com/Azure/azure-extension-platform v0.0.0-20240610175536-404c704f82f8/go.mod h1:nEQQIC3RKmMnpdc+RakYHIdu556jdcHv67ML8PdsQeQ=
github.com/Azure/azure-extension-platform v0.0.0-20250107200156-aa20f765d49f h1:ddsUz/suc9txCMz/xWOslqNMvzhbWFMTflUrbcMNoSw=
github.com/Azure/azure-extension-platform v0.0.0-20250107200156-aa20f765d49f/go.mod h1:0458BvQsi5ch6kn+KZtI5m88Z3L9UFXdoY1+6nKdivY=
github.com/Azure/azure-extension-platform v0.0.0-20260107210613-2a62cc200c34 h1:7bEC4DJC4w0gx7SBy7M7Q2qi6ckmHcnnlFJzo+X/gi4=
github.com/Azure/azure-extension-platform v0.0.0-20260107210613-2a62cc200c34/go.mod h1:0458BvQsi5ch6kn+KZtI5m88Z3L9UFXdoY1+6nKdivY=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
Expand Down
84 changes: 42 additions & 42 deletions internal/cmds/cmds.go

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions internal/cmds/cmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import (
"strings"
"testing"

osexec "os/exec"

"github.com/Azure/azure-extension-platform/pkg/extensionevents"
"github.com/Azure/azure-extension-platform/pkg/handlerenv"
"github.com/Azure/azure-extension-platform/pkg/logging"
"github.com/Azure/azure-extension-platform/vmextension"
"github.com/Azure/run-command-handler-linux/internal/constants"
"github.com/Azure/run-command-handler-linux/internal/exec"
"github.com/Azure/run-command-handler-linux/internal/files"
"github.com/Azure/run-command-handler-linux/internal/handlersettings"
"github.com/Azure/run-command-handler-linux/internal/settings"
Expand Down Expand Up @@ -377,7 +381,7 @@ func enable_extension(t *testing.T, fakeEnv types.HandlerEnvironment, tempDir st
err = encoder.Encode(handlerSettings)
require.Nil(t, err, "Could not serialze settings file")

RunCmd = func(ctx *log.Context, dir, scriptFilePath string, cfg *handlersettings.HandlerSettings, metadata types.RCMetadata) (error, int) {
RunCmd = func(ctx *log.Context, dir, scriptFilePath string, cfg *handlersettings.HandlerSettings, metadata types.RCMetadata) (*vmextension.ErrorWithClarification, int) {
wasCalled = true
return nil, 0 // mock behavior
}
Expand Down Expand Up @@ -435,6 +439,13 @@ func Test_runCmd_success(t *testing.T) {
require.Nil(t, err)
defer os.RemoveAll(dir)

orig := exec.FnRunCommand
defer func() { exec.FnRunCommand = orig }()
exec.FnRunCommand = func(_ *osexec.Cmd) error {
// Success
return nil
}

metadata := types.NewRCMetadata("extName", 0, constants.DownloadFolder, DataDir)
err, exitCode := runCmd(log.NewContext(log.NewNopLogger()), dir, "", &handlersettings.HandlerSettings{
PublicSettings: handlersettings.PublicSettings{Source: &handlersettings.ScriptSource{Script: script}},
Expand Down Expand Up @@ -582,7 +593,7 @@ func Test_downloadArtifactsFail(t *testing.T) {
})

require.NotNil(t, err)
require.Contains(t, err.Error(), "failed to download artifact")
require.Contains(t, err.Error(), "Failed to download artifact")
}

func Test_downloadArtifacts(t *testing.T) {
Expand Down Expand Up @@ -637,7 +648,7 @@ func Test_decodeScript(t *testing.T) {
testSubject := "bHMK"
s, info, err := decodeScript(testSubject)

require.NoError(t, err)
require.Nil(t, err)
require.Equal(t, info, "4;3;gzip=0")
require.Equal(t, s, "ls\n")
}
Expand All @@ -646,7 +657,7 @@ func Test_decodeScriptGzip(t *testing.T) {
testSubject := "H4sIACD731kAA8sp5gIAfShLWgMAAAA="
s, info, err := decodeScript(testSubject)

require.NoError(t, err)
require.Nil(t, err)
require.Equal(t, info, "32;3;gzip=1")
require.Equal(t, s, "ls\n")
}
Expand Down
37 changes: 26 additions & 11 deletions internal/commandProcessor/commandProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/Azure/azure-extension-platform/pkg/handlerenv"
"github.com/Azure/azure-extension-platform/pkg/logging"
"github.com/Azure/azure-extension-platform/vmextension"
"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"
Expand All @@ -22,6 +23,8 @@ import (

var (
handlerEnvironmentGetter func(name, version string) (he *handlerenv.HandlerEnvironment, _ error) = handlerenv.GetHandlerEnvironment
fnGetHandlerSettings = handlersettings.GetHandlerSettings
fnReportInstanceView = instanceview.ReportInstanceView
)

func ProcessImmediateHandlerCommand(cmd types.Cmd, hs handlersettings.HandlerSettingsFile, extensionName string, seqNum int) error {
Expand Down Expand Up @@ -71,17 +74,18 @@ func ProcessHandlerCommand(cmd types.Cmd) error {
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: "",
ExecutionState: types.Running,
ExecutionMessage: "Execution in progress",
ExitCode: 0,
Output: "",
Error: "",
StartTime: time.Now().UTC().Format(time.RFC3339),
EndTime: "",
ErrorClarificationValue: 0,
}

metadata := types.NewRCMetadata(extensionName, seqNum, downloadFolder, constants.DataDir)
instanceview.ReportInstanceView(ctx, hEnv, metadata, types.StatusTransitioning, cmd, &instView)
fnReportInstanceView(ctx, hEnv, metadata, types.StatusTransitioning, cmd, &instView)

// execute the subcommand
stdout, stderr, cmdInvokeError, exitCode := cmd.Functions.Invoke(ctx, hEnv, &instView, metadata, cmd)
Expand All @@ -96,13 +100,24 @@ func ProcessHandlerCommandWithDetails(ctx *log.Context, cmd types.Cmd, hEnv type
instView.ExitCode = exitCode
statusToReport := types.StatusSuccess

// Add an error clarification if we have one
var ewc *vmextension.ErrorWithClarification
if errors.As(cmdInvokeError, &ewc) {
instView.ErrorClarificationValue = ewc.ErrorCode
}

// 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)
cfg, err := fnGetHandlerSettings(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)
fnReportInstanceView(ctx, hEnv, metadata, statusToReport, cmd, &instView)

if err == nil {
return nil
}

return errors.Wrapf(err, "command execution failed")
} else { // No error. Succeeded
instView.ExecutionMessage = "Execution completed"
Expand All @@ -111,7 +126,7 @@ func ProcessHandlerCommandWithDetails(ctx *log.Context, cmd types.Cmd, hEnv type
instView.ExitCode = constants.ExitCode_Okay
}

instanceview.ReportInstanceView(ctx, hEnv, metadata, types.StatusSuccess, cmd, &instView)
fnReportInstanceView(ctx, hEnv, metadata, types.StatusSuccess, cmd, &instView)
ctx.Log("event", "end")

return nil
Expand Down
36 changes: 36 additions & 0 deletions internal/commandProcessor/commandProcessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"
"time"

"github.com/Azure/azure-extension-platform/vmextension"
"github.com/Azure/run-command-handler-linux/internal/cleanup"
"github.com/Azure/run-command-handler-linux/internal/constants"
"github.com/Azure/run-command-handler-linux/internal/handlersettings"
Expand Down Expand Up @@ -190,3 +191,38 @@ func Test_GetSeqNumFromFailedToFind(t *testing.T) {
require.Nil(t, err)
require.Equal(t, 0, actualSeqNum)
}

func Test_ProcessHandlerCommandWithDetails_Failure_WithClarification(t *testing.T) {
var last types.RunCommandInstanceView

origReportInstanceView := fnReportInstanceView
defer func() { fnReportInstanceView = origReportInstanceView }()
fnReportInstanceView = func(ctx *log.Context, hEnv types.HandlerEnvironment, metadata types.RCMetadata, t types.StatusType, c types.Cmd, instanceview *types.RunCommandInstanceView) error {
last = *instanceview
return nil
}

mockFunc := types.CmdFunctions{
Invoke: func(_ *log.Context, _ types.HandlerEnvironment, iv *types.RunCommandInstanceView, _ types.RCMetadata, _ types.Cmd) (string, string, error, int) {
return "x", "y", vmextension.NewErrorWithClarificationPtr(1234, errors.New("the chipmunks are upset")), 3
},
}

orig := fnGetHandlerSettings
defer func() { fnGetHandlerSettings = orig }()
fnGetHandlerSettings = func(string, string, int, *log.Context) (handlersettings.HandlerSettings, *vmextension.ErrorWithClarification) {
return handlersettings.HandlerSettings{
PublicSettings: handlersettings.PublicSettings{
TreatFailureAsDeploymentFailure: false,
},
}, nil
}

cmd := types.Cmd{Name: "run", FailExitCode: 3, Functions: mockFunc}
hEnv := types.HandlerEnvironment{}
ctx := log.NewContext(log.NewNopLogger())

err := ProcessHandlerCommandWithDetails(ctx, cmd, hEnv, "x", 1, "/tmp")
require.Nil(t, err, "expected no error because TreatFailureAsDeploymentFailure is false")
require.Equal(t, 1234, last.ErrorClarificationValue, "expected clarification 1234 but got %d", last.ErrorClarificationValue)
}
139 changes: 139 additions & 0 deletions internal/constants/errorclarification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package constants

const (
FileDownload_BadRequest = -41
FileDownload_UnknownError = -40
FileDownload_StorageError = -42
FileDownload_UnhandledError = -43
FileDownload_StorageClientInitialization = -44
FileDownload_CreateDirectoryFailure = -45
FileDownload_OpenFileForWriteFailure = -46
FileDownload_CouldNotCreateRequest = -47
FileDownload_InternalServerError = -48
FileDownload_WriteFileError = -49

Internal_CouldNotFindCertificate = -20
Internal_CouldNotDecrypt = -22
Internal_ArtifactCountMismatch = -23
Internal_ArtifactDoesNotExist = -24
Internal_IncorrectRunAsScriptPath = -25
Internal_RunAsOpenSourceScriptFileFailed = -26
Internal_RunAsCreateRunAsScriptFileFailed = -27
Internal_RunAsCopySourceScriptToRunAsScriptFileFailed = -28
Internal_RunAsLookupUserUidFailed = -29
Internal_RunAsScriptFileChangeOwnerFailed = -30
Internal_RunAsScriptFileChangePermissionsFailed = -31

Internal_CouldNotParseSettings = -32
Internal_InvalidHandlerSettingsJson = -33
Internal_InvalidHandlerSettingsCount = -34
Internal_NoHandlerSettingsThumbprint = -35
Internal_HandlerSettingsFailedToDecode = -36
Internal_DecryptingProtectedSettingsFailed = -37
Internal_UnmarshalProtectedSettingsFailed = -38
Internal_UnmarshalSettingsFailed = -39
Internal_UnmarshalPublicSettingsFailed = -40
Internal_InvalidArtifactSpecification = -41

Internal_CouldNotCreateStatusDirectory = -60
Internal_ExtensionDirectoryNameEmpty = -61
Internal_CouldNotOpenSubdirectory = -62
Internal_CouldNotReadDirectoryEntries = -63
Internal_FailedToOpenFileForReading = -64
Internal_FailedToCreateFile = -65
Internal_FailedToCopyFile = -66
Internal_FailedToReadFile = -67
Internal_CouldNotOpenFileForWriting = -68

Immediate_CouldNotDetermineServiceInstalled = -70
Immediate_CouldNotDetermineInstalledVersion = -71
Immediate_CouldNotMarkBinaryAsExecutable = -72
Immediate_CouldNotRemoveOldUnitConfigFile = -73
Immediate_ErrorCreatingUnitConfig = -74
Immediate_ErrorReloadingDaemonWorker = -75
Immediate_ErrorEnablingUnit = -76
Immediate_CouldNotStartService = -77
Immediate_CouldNotCheckServiceAlreadyEnabled = -78
Immediate_EnableServiceFailed = -79

Script_FailedToDecode = -101
Script_FailedToDecompress = -102

Hgap_FailedCreateRequest = -120
Hgap_CertificateMissingFromGoalState = -121
Hgap_NoCertThumbprint = -122
Hgap_FailedToCreateRequestFactory = -123
Hgap_FailedToParseAddress = -124
Hgap_EtagNotFound = -125
Hgap_FailedToParseImmediateSettings = -126
Hgap_CouldNotCreateRequestManager = -127
Hgap_InternalArgumentError = -128

HandlerEnv_CouldNotFindBaseDirectory = -140
HandlerEnv_HandlingError = -141
HandlerEnv_NotFound = -142
HandlerEnv_UnmarshalFailed = -143
HandlerEnv_InvalidConfigCount = -144

Msi_CouldNotDeserializeResponse = -90

Internal_UnknownError = -200

SystemError = -1 // CRP will interpret anything > 0 as a user error

// User errors
CommandExecution_BadConfig = 1
CommandExecution_FailureExitCode = 2
CommandExecution_TimedOut = 4
CommandExecution_RunAsCreateProcessFailed = 5
CommandExecution_RunAsUserLogonFailed = 6
CommandExecution_CouldNotStart = 7

CustomerInput_StorageCredsAndMIBothSpecified = 26
CustomerInput_ClientIdObjectIdBothSpecified = 27
CustomerInput_ErrorAndOutputBlobsSame = 28
CustomerInput_NoScriptSpecified = 29

FileDownload_AccessDenied = 52
FileDownload_DoesNotExist = 53
FileDownload_NetworkingError = 54
FileDownload_GenericError = 55
FileDownload_UnableToWriteFile = 57
ArtifactDownload_GenericError = 58
FileDownload_UnableToParseFileName = 59
FileDownload_CannotExtractFileNameFromUrl = 60
FileDownload_InvalidFileName = 61
FileDownload_FailedStatusCode = 62
FileDownload_CannotParseUrl = 63
FileDownload_CannotGenerateSasKey = 64
FileDownload_Empty = 65

Msi_NotFound = 70
Msi_DoesNotHaveRightPermissions = 71
Msi_GenericRetrievalError = 72

AppendBlobCreation_DoesNotExist = 90
AppendBlobCreation_PermissionsIssue = 91
AppendBlobCreation_Other = 92
AppendBlobCreation_InvalidUri = 93
AppendBlobCreation_InvalidMsi = 94
AppendBlobCreation_ObjectIdNotSupported = 95
AppendBlobCreation_ClientError = 96

ImmediateRC_ExceededConcurrentLimit = 100
ImmediateRC_TaskCanceled = 101
ImmediateRC_TaskTimeout = 102
ImmediateRC_UnknownFailure = 103
ImmediateRC_UnhandledException = 104
ImmediateRC_CommandSkipped = 105

FileSystem_CreateDataDirectoryFailed = 110
FileSystem_RemoveDataDirectoryFailed = 121
FileSystem_OpenStandardOutFailed = 122
FileSystem_OpenStandardErrorFailed = 123

Immediate_Systemd_NotSupported = 140

Http_RequestFailure = 150
Http_FailedStatusCode = 151
)
38 changes: 6 additions & 32 deletions internal/constants/exitcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,11 @@ const (
// Exit codes
ExitCode_Okay = 0

// User errors (-100s):
ExitCode_ScriptBlobDownloadFailed = -100
ExitCode_BlobCreateOrReplaceFailed = -101
ExitCode_RunAsLookupUserFailed = -102

// Service Errors (-200s):
ExitCode_CreateDataDirectoryFailed = -200
ExitCode_RemoveDataDirectoryFailed = -201
ExitCode_GetHandlerSettingsFailed = -202
ExitCode_SaveScriptFailed = -203
ExitCode_CommandExecutionFailed = -204
ExitCode_OpenStdOutFileFailed = -205
ExitCode_OpenStdErrFileFailed = -206
ExitCode_IncorrectRunAsScriptPath = -207
ExitCode_RunAsIncorrectScriptPath = -208
ExitCode_RunAsOpenSourceScriptFileFailed = -209
ExitCode_RunAsCreateRunAsScriptFileFailed = -210
ExitCode_RunAsCopySourceScriptToRunAsScriptFileFailed = -211
ExitCode_RunAsLookupUserUidFailed = -212
ExitCode_RunAsScriptFileChangeOwnerFailed = -213
ExitCode_RunAsScriptFileChangePermissionsFailed = -214
ExitCode_DownloadArtifactFailed = -215
ExitCode_UpgradeInstalledServiceFailed = -216
ExitCode_InstallServiceFailed = -217
ExitCode_UninstallInstalledServiceFailed = -218
ExitCode_DisableInstalledServiceFailed = -219
ExitCode_CopyStateForUpdateFailed = -220
ExitCode_SkippedImmediateGoalState = -221
ExitCode_ImmediateTaskTimeout = -222
ExitCode_ImmediateTaskFailed = -223
ExitCode_CouldNotRehydrateMrSeq = -224

// Unknown errors (-300s):
ExitCode_UpgradeInstalledServiceFailed = -216
ExitCode_InstallServiceFailed = -217
ExitCode_UninstallInstalledServiceFailed = -218
ExitCode_DisableInstalledServiceFailed = -219
ExitCode_CopyStateForUpdateFailed = -220
ExitCode_CouldNotRehydrateMrSeq = -224
)
Loading
Loading