fix: A single entities read failure wont clear all entities
This commit is contained in:
parent
b000e82238
commit
18903853fb
|
|
@ -104,7 +104,6 @@ func loadEntityFileJson(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ReadIn: %v", err)
|
log.Errorf("ReadIn: %v", err)
|
||||||
ClearEntitiesOfType(entityname)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +118,6 @@ func loadEntityFileJson(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("%v", err)
|
log.Errorf("%v", err)
|
||||||
ClearEntitiesOfType(entityname)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,7 +137,6 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ReadIn: %v", err)
|
log.Errorf("ReadIn: %v", err)
|
||||||
ClearEntitiesOfType(entityname)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,7 +146,6 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unmarshal: %v", err)
|
log.Errorf("Unmarshal: %v", err)
|
||||||
ClearEntitiesOfType(entityname)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -59,3 +61,34 @@ func TestGetEntityInstancesOrdered_emptyOrMissing(t *testing.T) {
|
||||||
ordered = GetEntityInstancesOrdered("empty_test")
|
ordered = GetEntityInstancesOrdered("empty_test")
|
||||||
assert.Nil(t, ordered)
|
assert.Nil(t, ordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadEntityFile_preservesEntitiesOnTransientFailure(t *testing.T) {
|
||||||
|
const entityName = "preserve_on_fail"
|
||||||
|
ClearEntitiesOfType(entityName)
|
||||||
|
defer ClearEntitiesOfType(entityName)
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
yamlPath := filepath.Join(dir, "hosts.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(yamlPath, []byte("- title: kept\n"), 0o600))
|
||||||
|
|
||||||
|
loadEntityFile(yamlPath, entityName)
|
||||||
|
require.Len(t, GetEntityInstancesOrdered(entityName), 1)
|
||||||
|
|
||||||
|
loadEntityFile(filepath.Join(dir, "missing.yaml"), entityName)
|
||||||
|
require.Len(t, GetEntityInstancesOrdered(entityName), 1, "read failure should keep last good entities")
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(yamlPath, []byte("not: valid: yaml: ["), 0o600))
|
||||||
|
loadEntityFile(yamlPath, entityName)
|
||||||
|
require.Len(t, GetEntityInstancesOrdered(entityName), 1, "parse failure should keep last good entities")
|
||||||
|
|
||||||
|
jsonPath := filepath.Join(dir, "hosts.json")
|
||||||
|
require.NoError(t, os.WriteFile(jsonPath, []byte("{\"title\":\"json-kept\"}\n"), 0o600))
|
||||||
|
loadEntityFile(jsonPath, entityName)
|
||||||
|
require.Len(t, GetEntityInstancesOrdered(entityName), 1)
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(jsonPath, []byte("{bad json"), 0o600))
|
||||||
|
loadEntityFile(jsonPath, entityName)
|
||||||
|
ordered := GetEntityInstancesOrdered(entityName)
|
||||||
|
require.Len(t, ordered, 1, "JSON parse failure should keep last good entities")
|
||||||
|
assert.Equal(t, "json-kept", ordered[0].Title)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1391,6 +1391,11 @@ func triggerLoop(req *ExecutionRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepSaveLog(req *ExecutionRequest) bool {
|
func stepSaveLog(req *ExecutionRequest) bool {
|
||||||
|
if !canSaveExecutionLog(req) {
|
||||||
|
log.Warnf("Cannot save execution log; missing request, log entry, binding/action, or config")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
filename := fmt.Sprintf("%v.%v.%v", sanitizeLogFilename(req.logEntry.ActionTitle), req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
|
filename := fmt.Sprintf("%v.%v.%v", sanitizeLogFilename(req.logEntry.ActionTitle), req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
|
||||||
|
|
||||||
saveLogResults(req, filename)
|
saveLogResults(req, filename)
|
||||||
|
|
@ -1399,10 +1404,14 @@ func stepSaveLog(req *ExecutionRequest) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canSaveExecutionLog(req *ExecutionRequest) bool {
|
||||||
|
return req != nil && req.logEntry != nil && req.Binding != nil && req.Binding.Action != nil && req.Cfg != nil
|
||||||
|
}
|
||||||
|
|
||||||
// sanitizeLogFilename replaces characters that are unsafe in filenames so action
|
// sanitizeLogFilename replaces characters that are unsafe in filenames so action
|
||||||
// titles like "Create/update Report" do not create nested paths or fail to write.
|
// titles like "Create/update Report" do not create nested paths or fail to write.
|
||||||
func sanitizeLogFilename(title string) string {
|
func sanitizeLogFilename(title string) string {
|
||||||
replacer := strings.NewReplacer(
|
oldnew := []string{
|
||||||
"/", "_",
|
"/", "_",
|
||||||
"\\", "_",
|
"\\", "_",
|
||||||
":", "_",
|
":", "_",
|
||||||
|
|
@ -1412,9 +1421,15 @@ func sanitizeLogFilename(title string) string {
|
||||||
"<", "_",
|
"<", "_",
|
||||||
">", "_",
|
">", "_",
|
||||||
"|", "_",
|
"|", "_",
|
||||||
)
|
}
|
||||||
|
|
||||||
return replacer.Replace(title)
|
// NUL and other C0 controls plus DEL are invalid or problematic in filenames.
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
oldnew = append(oldnew, string(rune(i)), "_")
|
||||||
|
}
|
||||||
|
oldnew = append(oldnew, "\x7f", "_")
|
||||||
|
|
||||||
|
return strings.NewReplacer(oldnew...).Replace(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstNonEmpty(one, two string) string {
|
func firstNonEmpty(one, two string) string {
|
||||||
|
|
|
||||||
|
|
@ -790,6 +790,8 @@ func TestSanitizeLogFilename(t *testing.T) {
|
||||||
{"Create/update Monthly Report", "Create_update Monthly Report"},
|
{"Create/update Monthly Report", "Create_update Monthly Report"},
|
||||||
{`path\with\backslashes`, "path_with_backslashes"},
|
{`path\with\backslashes`, "path_with_backslashes"},
|
||||||
{`a:b*c?d"e<f>g|h`, "a_b_c_d_e_f_g_h"},
|
{`a:b*c?d"e<f>g|h`, "a_b_c_d_e_f_g_h"},
|
||||||
|
{"has\x00nul", "has_nul"},
|
||||||
|
{"tab\there\nand\rreturn", "tab_here_and_return"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
@ -870,3 +872,78 @@ func TestStepSaveLogKeepsSafeTitleFilename(t *testing.T) {
|
||||||
expectedPath := filepath.Join(resultsDir, "Echo Test.1714333384."+trackingID+".yaml")
|
expectedPath := filepath.Join(resultsDir, "Echo Test.1714333384."+trackingID+".yaml")
|
||||||
assert.FileExists(t, expectedPath)
|
assert.FileExists(t, expectedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepSaveLogSanitizesNULInTitle(t *testing.T) {
|
||||||
|
resultsDir := t.TempDir()
|
||||||
|
outputDir := t.TempDir()
|
||||||
|
started := time.Unix(1714333384, 0)
|
||||||
|
trackingID := "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
|
||||||
|
title := "Bad\x00Title"
|
||||||
|
|
||||||
|
req := &ExecutionRequest{
|
||||||
|
Cfg: &config.Config{
|
||||||
|
SaveLogs: config.SaveLogsConfig{
|
||||||
|
ResultsDirectory: resultsDir,
|
||||||
|
OutputDirectory: outputDir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Binding: &ActionBinding{
|
||||||
|
Action: &config.Action{},
|
||||||
|
},
|
||||||
|
logEntry: &InternalLogEntry{
|
||||||
|
ActionTitle: title,
|
||||||
|
DatetimeStarted: started,
|
||||||
|
ExecutionTrackingID: trackingID,
|
||||||
|
Output: "nul ok",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepSaveLog(req))
|
||||||
|
|
||||||
|
expectedBase := "Bad_Title.1714333384." + trackingID
|
||||||
|
resultsPath := filepath.Join(resultsDir, expectedBase+".yaml")
|
||||||
|
outputPath := filepath.Join(outputDir, expectedBase+".log")
|
||||||
|
|
||||||
|
assert.FileExists(t, resultsPath)
|
||||||
|
assert.FileExists(t, outputPath)
|
||||||
|
assert.NotContains(t, resultsPath, "\x00")
|
||||||
|
assert.NotContains(t, outputPath, "\x00")
|
||||||
|
|
||||||
|
output, err := os.ReadFile(outputPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "nul ok", string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepSaveLogReturnsFalseWhenDependenciesMissing(t *testing.T) {
|
||||||
|
started := time.Unix(1714333384, 0)
|
||||||
|
valid := &ExecutionRequest{
|
||||||
|
Cfg: &config.Config{},
|
||||||
|
Binding: &ActionBinding{
|
||||||
|
Action: &config.Action{},
|
||||||
|
},
|
||||||
|
logEntry: &InternalLogEntry{
|
||||||
|
ActionTitle: "Echo",
|
||||||
|
DatetimeStarted: started,
|
||||||
|
ExecutionTrackingID: "cccccccc-dddd-eeee-ffff-000000000000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, stepSaveLog(nil))
|
||||||
|
assert.False(t, stepSaveLog(&ExecutionRequest{}))
|
||||||
|
|
||||||
|
missingLog := *valid
|
||||||
|
missingLog.logEntry = nil
|
||||||
|
assert.False(t, stepSaveLog(&missingLog))
|
||||||
|
|
||||||
|
missingBinding := *valid
|
||||||
|
missingBinding.Binding = nil
|
||||||
|
assert.False(t, stepSaveLog(&missingBinding))
|
||||||
|
|
||||||
|
missingAction := *valid
|
||||||
|
missingAction.Binding = &ActionBinding{}
|
||||||
|
assert.False(t, stepSaveLog(&missingAction))
|
||||||
|
|
||||||
|
missingCfg := *valid
|
||||||
|
missingCfg.Cfg = nil
|
||||||
|
assert.False(t, stepSaveLog(&missingCfg))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue