Skip to content

Commit 11fc7b2

Browse files
RottenRatSinelnikov Michailldmonster
authored
[modules] 'Enabled' scripts add reason (#636)
Signed-off-by: Sinelnikov Michail <mikhail.sinelnikov@flant.com> Co-authored-by: Sinelnikov Michail <mikhail.sinelnikov@flant.com> Co-authored-by: Pavel Okhlopkov <36456348+ldmonster@users.noreply.github.com>
1 parent 696fddc commit 11fc7b2

File tree

4 files changed

+134
-3
lines changed

4 files changed

+134
-3
lines changed

docs/src/LIFECYCLE-STEPS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ Startup steps:
8989
- '{moduleName}' section in ConfigMap
9090
- patched with patches saved from previous module hooks
9191
- output
92-
- 'enabled' state ($MODULE_ENABLED_RESULT temporary file)
93-
- if hook return "false", hook state is disabled
92+
- **module status**
93+
- `$MODULE_ENABLED_RESULT` - *required* file, script writes `true` or `false`
94+
- if `$MODULE_ENABLED_RESULT` contains `false`, the module is considered disabled
95+
- `$MODULE_ENABLED_REASON` - *optional* file, the script can put a human-readable reason string
9496
- no 'enabled' script
9597
- merged 'enabled' state is used
9698
- create 3 lists

docs/src/LIFECYCLE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ As a result of a 'module discovery' process, the tasks for the execution of all
8888

8989
### Enabled script
9090

91-
A script or an executable file that returns the status of the module. The script has access to the module values in `$VALUES_PATH` and `$CONFIG_VALUES_PATH` files, more details about the values are available [here](VALUES.md#using-the-values-in-enabled-scripts). The variable `$MODULE_ENABLED_RESULT` passes the path to the file into which the script should write the module status: `true` or `false`.
91+
A script or an executable file that returns the status of the module. The script has access to the module values in `$VALUES_PATH` and `$CONFIG_VALUES_PATH` files, more details about the values are available [here](VALUES.md#using-the-values-in-enabled-scripts).
92+
93+
The addon-operator exports two environment variables:
94+
**`$MODULE_ENABLED_RESULT`***required* path to a temporary file; Write `true` or `false` here.
95+
**`$MODULE_ENABLED_REASON`***optional* path to another file; write a **explanation** when the script decides to disable the module.
9296

9397
Below is an example of the `enabled` script that disables the module when parameter `param2` is set to "stopMePlease".
9498

@@ -99,6 +103,7 @@ param2=$(jq -r '.simpleModule.param2' $VALUES_PATH)
99103

100104
if [[ $param2 == "stopMePlease" ]] ; then
101105
echo "false" > $MODULE_ENABLED_RESULT
106+
echo "Module disabled: param2 == stopMePlease" > "$MODULE_ENABLED_REASON"
102107
else
103108
echo "true" > $MODULE_ENABLED_RESULT
104109
fi

pkg/module_manager/models/modules/basic.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,26 @@ func (bm *BasicModule) RunEnabledScript(ctx context.Context, tmpDir string, prec
762762
}
763763
}()
764764

765+
reasonFilePath, err := bm.prepareModuleEnabledReasonFile(tmpDir)
766+
if err != nil {
767+
logEntry.Error("Prepare MODULE_ENABLED_REASON file",
768+
slog.String("path", reasonFilePath),
769+
log.Err(err))
770+
return false, err
771+
}
772+
defer func() {
773+
if bm.keepTemporaryHookFiles {
774+
return
775+
}
776+
err := os.Remove(reasonFilePath)
777+
if err != nil {
778+
bm.logger.With("module", bm.GetName()).
779+
Error("Remove tmp file",
780+
slog.String("path", enabledResultFilePath),
781+
log.Err(err))
782+
}
783+
}()
784+
765785
logEntry.Debug("Execute enabled script",
766786
slog.String("path", enabledScriptPath),
767787
slog.Any("modules", precedingEnabledModules))
@@ -771,6 +791,7 @@ func (bm *BasicModule) RunEnabledScript(ctx context.Context, tmpDir string, prec
771791
envs = append(envs, fmt.Sprintf("CONFIG_VALUES_PATH=%s", configValuesPath))
772792
envs = append(envs, fmt.Sprintf("VALUES_PATH=%s", valuesPath))
773793
envs = append(envs, fmt.Sprintf("MODULE_ENABLED_RESULT=%s", enabledResultFilePath))
794+
envs = append(envs, fmt.Sprintf("MODULE_ENABLED_REASON=%s", reasonFilePath))
774795

775796
if err := bm.AssembleEnvironmentForModule(environmentmanager.EnabledScriptEnvironment); err != nil {
776797
return false, fmt.Errorf("Assemble %q module's environment: %w", bm.GetName(), err)
@@ -821,8 +842,21 @@ func (bm *BasicModule) RunEnabledScript(ctx context.Context, tmpDir string, prec
821842
logEntry.Info("Enabled script run successful",
822843
slog.Bool("result", moduleEnabled),
823844
slog.String("status", result))
845+
var reason string
846+
if !moduleEnabled {
847+
reason, err = bm.readModuleEnabledReason(reasonFilePath)
848+
if err != nil {
849+
logEntry.Error("Read enabled result",
850+
slog.String("path", enabledScriptPath),
851+
log.Err(err))
852+
return false, fmt.Errorf("bad enabled result")
853+
}
854+
}
824855
bm.l.Lock()
825856
bm.state.enabledScriptResult = &moduleEnabled
857+
if reason != "" {
858+
bm.state.enabledScriptReason = &reason
859+
}
826860
bm.l.Unlock()
827861
return moduleEnabled, nil
828862
}
@@ -840,6 +874,14 @@ func (bm *BasicModule) prepareModuleEnabledResultFile(tmpdir string) (string, er
840874
return path, nil
841875
}
842876

877+
func (bm *BasicModule) prepareModuleEnabledReasonFile(tmpdir string) (string, error) {
878+
path := filepath.Join(tmpdir, fmt.Sprintf("%s.module-enabled-reason", bm.GetName()))
879+
if err := utils.CreateEmptyWritableFile(path); err != nil {
880+
return "", err
881+
}
882+
return path, nil
883+
}
884+
843885
func (bm *BasicModule) readModuleEnabledResult(filePath string) (bool, error) {
844886
data, err := os.ReadFile(filePath)
845887
if err != nil {
@@ -858,6 +900,14 @@ func (bm *BasicModule) readModuleEnabledResult(filePath string) (bool, error) {
858900
return false, fmt.Errorf("expected 'true' or 'false', got '%s'", value)
859901
}
860902

903+
func (bm *BasicModule) readModuleEnabledReason(path string) (string, error) {
904+
data, err := os.ReadFile(path)
905+
if err != nil {
906+
return "", fmt.Errorf("cannot read %s: %w", path, err)
907+
}
908+
return strings.TrimSpace(string(data)), nil
909+
}
910+
861911
// VALUES_PATH
862912
func (bm *BasicModule) prepareValuesJsonFileWith(tmpdir string, values utils.Values) (string, error) {
863913
data, err := values.JsonBytes()
@@ -1220,6 +1270,13 @@ func (bm *BasicModule) GetEnabledScriptResult() *bool {
12201270
return bm.state.enabledScriptResult
12211271
}
12221272

1273+
// GetEnabledScriptReason returns a string why the module is disabled
1274+
func (bm *BasicModule) GetEnabledScriptReason() *string {
1275+
bm.l.RLock()
1276+
defer bm.l.RUnlock()
1277+
return bm.state.enabledScriptReason
1278+
}
1279+
12231280
// GetLastHookError get error of the last executed hook
12241281
func (bm *BasicModule) GetLastHookError() error {
12251282
bm.l.RLock()
@@ -1358,4 +1415,5 @@ type moduleState struct {
13581415
hookErrors map[string]error
13591416
synchronizationState *SynchronizationState
13601417
enabledScriptResult *bool
1418+
enabledScriptReason *string
13611419
}

pkg/module_manager/models/modules/basic_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,69 @@ fi
234234
err = IsFileBatchHook("moduleName", hookPath, fileInfo)
235235
require.NoError(t, err)
236236
}
237+
238+
func stubDeps(logger *log.Logger) *hooks.HookExecutionDependencyContainer {
239+
st := metric_storage.NewMetricStorage(context.TODO(), "addon_operator_", false, logger)
240+
return &hooks.HookExecutionDependencyContainer{
241+
HookMetricsStorage: st,
242+
MetricStorage: st,
243+
KubeObjectPatcher: objectpatch.NewObjectPatcher(nil, logger),
244+
GlobalValuesGetter: &noopValuesGetter{},
245+
KubeConfigManager: &noopConfigManager{},
246+
}
247+
}
248+
249+
func TestRunEnabledScriptTrue(t *testing.T) {
250+
tmpModuleDir := t.TempDir()
251+
enabledPath := filepath.Join(tmpModuleDir, "enabled")
252+
253+
require.NoError(t, os.WriteFile(enabledPath, []byte(`#!/bin/sh
254+
echo "true" > "$MODULE_ENABLED_RESULT"
255+
exit 0
256+
`), 0o755))
257+
258+
bm, err := NewBasicModule("example", tmpModuleDir, 1, utils.Values{}, nil, nil)
259+
require.NoError(t, err)
260+
261+
logger := log.NewLogger()
262+
bm.WithLogger(logger)
263+
bm.WithDependencies(stubDeps(logger))
264+
265+
ok, err := bm.RunEnabledScript(context.TODO(), t.TempDir(), nil, map[string]string{})
266+
require.NoError(t, err)
267+
require.True(t, ok, "Module should be enabled")
268+
269+
require.NotNil(t, bm.GetEnabledScriptResult())
270+
require.True(t, *bm.GetEnabledScriptResult())
271+
r := bm.GetEnabledScriptReason()
272+
require.Nil(t, r)
273+
}
274+
275+
func TestRunEnabledScriptFalseWithReason(t *testing.T) {
276+
tmpModuleDir := t.TempDir()
277+
enabledPath := filepath.Join(tmpModuleDir, "enabled")
278+
279+
require.NoError(t, os.WriteFile(enabledPath, []byte(`#!/bin/sh
280+
echo "false" > "$MODULE_ENABLED_RESULT"
281+
echo "Kubernetes version is too low" > "$MODULE_ENABLED_REASON"
282+
exit 0
283+
`), 0o755))
284+
285+
bm, err := NewBasicModule("example", tmpModuleDir, 1, utils.Values{}, nil, nil)
286+
require.NoError(t, err)
287+
288+
logger := log.NewLogger()
289+
bm.WithLogger(logger)
290+
bm.WithDependencies(stubDeps(logger))
291+
292+
ok, err := bm.RunEnabledScript(context.TODO(), t.TempDir(), nil, map[string]string{})
293+
require.NoError(t, err)
294+
require.False(t, ok, "Module should be disabled")
295+
296+
require.NotNil(t, bm.GetEnabledScriptResult())
297+
require.False(t, *bm.GetEnabledScriptResult())
298+
299+
erGetter := bm.GetEnabledScriptReason()
300+
require.NotNil(t, erGetter)
301+
require.Equal(t, "Kubernetes version is too low", *erGetter)
302+
}

0 commit comments

Comments
 (0)