From 7bba12cad78141cb06a226568a851e02471b8a73 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 28 Aug 2025 01:18:24 +0200 Subject: [PATCH 01/15] Improve loader. Better support env variables. Force array for a command, plain string is never used and adds complexity. --- internal/launchr/env.go | 52 ++++++++++++++++++++-- internal/launchr/env_test.go | 81 +++++++++++++++++++++++++++++++++++ pkg/action/loader_test.go | 4 +- pkg/action/yaml.def.go | 30 +++---------- pkg/action/yaml_const_test.go | 35 +++++++++------ pkg/action/yaml_test.go | 5 ++- 6 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 internal/launchr/env_test.go diff --git a/internal/launchr/env.go b/internal/launchr/env.go index 8ccc5aa..0e91d70 100644 --- a/internal/launchr/env.go +++ b/internal/launchr/env.go @@ -56,11 +56,55 @@ func Getenv(key string) string { if key == "$" { return "$" } - // Replace all subexpressions. - if strings.Contains(key, "$") { - key = os.Expand(key, Getenv) + + // Condition functions for expansion patterns + var ( + varExists = func(v string, exists bool) bool { return exists } + varExistsNotEmpty = func(v string, exists bool) bool { return exists && v != "" } + ) + + // Handle shell parameter expansion patterns + if idx := strings.Index(key, ":-"); idx != -1 { + // ${var:-default} - use default if variable doesn't exist or is empty + return envHandleExpansion(key[:idx], key[idx+2:], varExistsNotEmpty, true) + } + if idx := strings.Index(key, "-"); idx != -1 { + // ${var-default} - use default if variable doesn't exist + return envHandleExpansion(key[:idx], key[idx+1:], varExists, true) + } + if idx := strings.Index(key, ":+"); idx != -1 { + // ${var:+alternative} - use alternative if variable exists and is not empty + return envHandleExpansion(key[:idx], key[idx+2:], varExistsNotEmpty, false) } - // @todo implement ${var-$DEFAULT}, ${var:-$DEFAULT}, ${var+$DEFAULT}, ${var:+$DEFAULT}, + if idx := strings.Index(key, "+"); idx != -1 { + // ${var+alternative} - use alternative if variable exists + return envHandleExpansion(key[:idx], key[idx+1:], varExists, false) + } + + // Regular environment variable lookup v, _ := syscall.Getenv(key) return v } + +// envHandleExpansion handles all expansion patterns +func envHandleExpansion(varName, value string, condition func(string, bool) bool, useVarValue bool) string { + envValue, exists := syscall.Getenv(varName) + if condition(envValue, exists) { + if useVarValue { + return envValue + } + return envExpandValue(value) + } + if useVarValue { + return envExpandValue(value) + } + return "" +} + +// envExpandValue expands variables and nested expressions in the value +func envExpandValue(value string) string { + if strings.Contains(value, "$") { + return os.Expand(value, Getenv) + } + return value +} diff --git a/internal/launchr/env_test.go b/internal/launchr/env_test.go new file mode 100644 index 0000000..df7ccd9 --- /dev/null +++ b/internal/launchr/env_test.go @@ -0,0 +1,81 @@ +package launchr + +import ( + "os" + "syscall" + "testing" +) + +func TestGetenv(t *testing.T) { + // Set up test environment variables + _ = syscall.Setenv("TEST_EXISTING_VAR", "existing_value") + _ = syscall.Setenv("TEST_EMPTY_VAR", "") + _ = syscall.Setenv("TEST_DEFAULT", "default_from_env") + defer func() { + _ = syscall.Unsetenv("TEST_EXISTING_VAR") + _ = syscall.Unsetenv("TEST_EMPTY_VAR") + _ = syscall.Unsetenv("TEST_DEFAULT") + }() + + tests := []struct { + name string + input string + expected string + }{ + // Basic cases + {"dollar sign", "$", "$"}, + {"existing variable", "TEST_EXISTING_VAR", "existing_value"}, + {"non-existing variable", "TEST_NON_EXISTING", ""}, + + // ${var-TEST_DEFAULT} - use TEST_DEFAULT if variable doesn't exist + {"var-TEST_DEFAULT with existing var", "TEST_EXISTING_VAR-fallback", "existing_value"}, + {"var-TEST_DEFAULT with non-existing var", "TEST_NON_EXISTING-fallback", "fallback"}, + {"var-TEST_DEFAULT with empty var", "TEST_EMPTY_VAR-fallback", ""}, + + // ${var:-TEST_DEFAULT} - use TEST_DEFAULT if variable doesn't exist or is empty + {"var:-TEST_DEFAULT with existing var", "TEST_EXISTING_VAR:-fallback", "existing_value"}, + {"var:-TEST_DEFAULT with non-existing var", "TEST_NON_EXISTING:-fallback", "fallback"}, + {"var:-TEST_DEFAULT with empty var", "TEST_EMPTY_VAR:-fallback", "fallback"}, + + // ${var+alternative} - use alternative if variable exists + {"var+alt with existing var", "TEST_EXISTING_VAR+alternative", "alternative"}, + {"var+alt with non-existing var", "TEST_NON_EXISTING+alternative", ""}, + {"var+alt with empty var", "TEST_EMPTY_VAR+alternative", "alternative"}, + + // ${var:+alternative} - use alternative if variable exists and is not empty + {"var:+alt with existing var", "TEST_EXISTING_VAR:+alternative", "alternative"}, + {"var:+alt with non-existing var", "TEST_NON_EXISTING:+alternative", ""}, + {"var:+alt with empty var", "TEST_EMPTY_VAR:+alternative", ""}, + + // Variable expansion in TEST_DEFAULT/alternative values + {"var-$VAR with variable expansion", "TEST_NON_EXISTING-$TEST_DEFAULT", "default_from_env"}, + {"var:-$VAR with variable expansion", "TEST_NON_EXISTING:-$TEST_DEFAULT", "default_from_env"}, + {"var+$VAR with variable expansion", "TEST_EXISTING_VAR+$TEST_DEFAULT", "default_from_env"}, + {"var:+$VAR with variable expansion", "TEST_EXISTING_VAR:+$TEST_DEFAULT", "default_from_env"}, + + // Complex case from your example + {"complex TEST_DEFAULT with variable", "TEST_NON_EXISTING:-MyValue_$TEST_DEFAULT", "MyValue_default_from_env"}, + + // Nested expressions - should not recursively expand + {"nested expression", "TEST_NON_EXISTING:-${TEST_EXISTING_VAR:+fallback}", "fallback"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Getenv(tt.input) + if result != tt.expected { + t.Errorf("Getenv(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } + + // Test with os.Expand integration + t.Run("os.Expand integration", func(t *testing.T) { + testString := "${TEST_NON_EXISTING:-MyValue_$TEST_DEFAULT}" + result := os.Expand(testString, Getenv) + expected := "MyValue_default_from_env" + if result != expected { + t.Errorf("os.Expand(%q, Getenv) = %q, want %q", testString, result, expected) + } + }) +} diff --git a/pkg/action/loader_test.go b/pkg/action/loader_test.go index f49878d..4555171 100644 --- a/pkg/action/loader_test.go +++ b/pkg/action/loader_test.go @@ -45,10 +45,10 @@ func Test_EnvProcessor(t *testing.T) { }() _ = os.Setenv("TEST_ENV1", "VAL1") _ = os.Setenv("TEST_ENV2", "VAL2") - s := "$TEST_ENV1$TEST_ENV1,${TEST_ENV2},$$TEST_ENV1,${TEST_ENV_UNDEF},${TODO-$TEST_ENV1},${TODO:-$TEST_ENV1},${TODO+$TEST_ENV1},${TODO:+$TEST_ENV1}" + s := "$TEST_ENV1$TEST_ENV1,${TEST_ENV2},$$TEST_ENV1,${TEST_ENV_UNDEF},${TEST_ENV_UNDEF-$TEST_ENV1},${TEST_ENV_UNDEF:-$TEST_ENV2},${TEST_ENV2+$TEST_ENV1},${TEST_ENV1:+$TEST_ENV2}" res, err := proc.Process(LoadContext{Action: act}, []byte(s)) assert.NoError(t, err) - assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENV1,,,,,", string(res)) + assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENV1,,VAL1,VAL2,VAL1,VAL2", string(res)) // Test action predefined env variables. s = "$CBIN,$ACTION_ID,$ACTION_WD,$ACTION_DIR,$DISCOVERY_DIR" res, err = proc.Process(LoadContext{Action: act}, []byte(s)) diff --git a/pkg/action/yaml.def.go b/pkg/action/yaml.def.go index 02f7063..ab44b93 100644 --- a/pkg/action/yaml.def.go +++ b/pkg/action/yaml.def.go @@ -187,7 +187,7 @@ func (r *DefRuntimeType) UnmarshalYAML(n *yaml.Node) (err error) { // DefRuntimeContainer has container-specific runtime configuration. type DefRuntimeContainer struct { - Command StrSliceOrStr `yaml:"command"` + Command StrSlice `yaml:"command"` Image string `yaml:"image"` Build *driver.BuildDefinition `yaml:"build"` ExtraHosts StrSlice `yaml:"extra_hosts"` @@ -281,7 +281,7 @@ func (r *DefRuntime) UnmarshalYAML(n *yaml.Node) (err error) { } } -// StrSlice is an array of strings for command execution. +// StrSlice is an array of strings. type StrSlice []string // UnmarshalYAML implements [yaml.Unmarshaler] to parse a string or a list of strings. @@ -289,33 +289,13 @@ func (l *StrSlice) UnmarshalYAML(n *yaml.Node) (err error) { if n.Kind == yaml.ScalarNode { return yamlTypeErrorLine(sErrArrEl, n.Line, n.Column) } - var s StrSliceOrStr - err = n.Decode(&s) - if err != nil { - return err - } - *l = StrSlice(s) - return err -} - -// StrSliceOrStr is an array of strings for command execution. -type StrSliceOrStr []string - -// UnmarshalYAML implements [yaml.Unmarshaler] to parse a string or a list of strings. -func (l *StrSliceOrStr) UnmarshalYAML(n *yaml.Node) (err error) { - type yamlT StrSliceOrStr - if n.Kind == yaml.ScalarNode { - var s string - err = n.Decode(&s) - *l = StrSliceOrStr{s} - return err - } + type yamlT StrSlice var s yamlT err = n.Decode(&s) if err != nil { - return yamlTypeErrorLine(sErrArrOrStrEl, n.Line, n.Column) + return yamlTypeErrorLine(sErrArrEl, n.Line, n.Column) } - *l = StrSliceOrStr(s) + *l = StrSlice(s) return err } diff --git a/pkg/action/yaml_const_test.go b/pkg/action/yaml_const_test.go index df9decf..a128841 100644 --- a/pkg/action/yaml_const_test.go +++ b/pkg/action/yaml_const_test.go @@ -94,6 +94,15 @@ runtime: - for i in $(seq 3); do echo $$i; sleep 1; done ` +const invalidCmdStringYaml = ` +action: + title: Title +runtime: + type: container + image: python:3.7-slim + command: pwd +` + const invalidCmdObjYaml = ` action: title: Title @@ -129,7 +138,7 @@ const invalidEmptyImgYaml = ` version: action: title: Title - command: python3 + command: [pwd] runtime: type: container ` @@ -140,7 +149,7 @@ action: title: Title runtime: type: container - command: python3 + command: [pwd] image: "" ` @@ -302,7 +311,7 @@ runtime: type: container image: python:3.7-slim build: ./ - command: ls + command: [pwd] ` const validBuildImgLongYaml = ` @@ -320,7 +329,7 @@ runtime: tags: - my/image:v1 - my/image:v2 - command: ls + command: [pwd] ` // Extra hosts key. @@ -333,7 +342,7 @@ runtime: extra_hosts: - "host.docker.internal:host-gateway" - "example.com:127.0.0.1" - command: ls + command: [pwd] ` const invalidExtraHostsYaml = ` @@ -343,7 +352,7 @@ runtime: type: container image: python:3.7-slim extra_hosts: "host.docker.internal:host-gateway" - command: ls + command: [pwd] ` // Environmental variables. @@ -353,7 +362,7 @@ action: runtime: type: container image: my/image:v1 - command: ls + command: [pwd] env: - MY_ENV_1=test1 - MY_ENV_2=test2 @@ -365,7 +374,7 @@ action: runtime: type: container image: my/image:v1 - command: ls + command: [pwd] env: MY_ENV_1: test1 MY_ENV_2: test2 @@ -377,7 +386,7 @@ action: runtime: type: container image: my/image:v1 - command: ls + command: [pwd] env: - MY_ENV_1=test1 MY_ENV_2: test2 @@ -389,7 +398,7 @@ action: runtime: type: container image: my/image:v1 - command: ls + command: [pwd] env: MY_ENV_1=test1 ` @@ -399,7 +408,7 @@ action: runtime: type: container image: my/image:v1 - command: ls + command: [pwd] env: MY_ENV_1: { MY_ENV_2: test2 } ` @@ -410,8 +419,8 @@ action: title: Title runtime: type: container - image: {{ .A1 }} - command: {{ .A1 }} + image: {{ .A1 }} + command: [pwd] env: - {{ .A2 }} {{ .A3 }} - {{ .A2 }} {{ .A3 }} asafs diff --git a/pkg/action/yaml_test.go b/pkg/action/yaml_test.go index 155b3b5..57c3bf1 100644 --- a/pkg/action/yaml_test.go +++ b/pkg/action/yaml_test.go @@ -58,8 +58,9 @@ func Test_CreateFromYaml(t *testing.T) { // Command declaration as array of strings. {"valid command - strings array", validCmdArrYaml, nil}, - {"invalid command - object", invalidCmdObjYaml, yamlTypeErrorLine(sErrArrOrStrEl, 8, 5)}, - {"invalid command - various array", invalidCmdArrVarYaml, yamlTypeErrorLine(sErrArrOrStrEl, 8, 5)}, + {"invalid command - string", invalidCmdStringYaml, yamlTypeErrorLine(sErrArrEl, 7, 12)}, + {"invalid command - object", invalidCmdObjYaml, yamlTypeErrorLine(sErrArrEl, 8, 5)}, + {"invalid command - various array", invalidCmdArrVarYaml, yamlTypeErrorLine(sErrArrEl, 8, 5)}, // Build image. {"build image - short", validBuildImgShortYaml, nil}, From 5ce3349a968f692d2a2df04e9b7d7e6cb9145840 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Fri, 29 Aug 2025 00:02:30 +0200 Subject: [PATCH 02/15] Improve loader. Better support env variables. Force array for a command, plain string is never used and adds complexity. --- internal/launchr/env.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/launchr/env.go b/internal/launchr/env.go index 0e91d70..8692e7d 100644 --- a/internal/launchr/env.go +++ b/internal/launchr/env.go @@ -59,7 +59,7 @@ func Getenv(key string) string { // Condition functions for expansion patterns var ( - varExists = func(v string, exists bool) bool { return exists } + varExists = func(_ string, exists bool) bool { return exists } varExistsNotEmpty = func(v string, exists bool) bool { return exists && v != "" } ) From cad4dac8f74c28eee815ce0d308594cd2b26de7c Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 18 Sep 2025 23:15:37 +0200 Subject: [PATCH 03/15] Rework loader. Execute templates only on string values instead of a whole file. Add support for multiline values in action.yaml. Add new template functions: default, config.Get Deprecate removeLine and others because we can't delete a line with the new processing. It conflicts with multiline support. Refactor out Services for better testing. --- app.go | 50 +-- docs/actions.md | 2 +- docs/actions.schema.md | 70 ++-- docs/development/plugin.md | 2 +- docs/development/service.md | 6 +- example/actions/arguments/action.yaml | 10 +- .../platform/actions/build/action.yaml | 16 +- internal/launchr/config.go | 13 +- internal/launchr/services.go | 81 +++++ internal/launchr/types.go | 24 +- pkg/action/action.go | 28 +- pkg/action/action.input.go | 55 +++ pkg/action/action_test.go | 25 +- pkg/action/loader.go | 322 +++++++++++------- pkg/action/loader_test.go | 101 ++---- pkg/action/manager.go | 83 +---- pkg/action/process.go | 117 +++++++ pkg/action/process_test.go | 13 +- pkg/action/test_utils.go | 4 +- pkg/action/yaml.def.go | 26 +- pkg/action/yaml.discovery.go | 62 +--- pkg/action/yaml_const_test.go | 34 +- pkg/action/yaml_test.go | 26 +- plugins/actionnaming/plugin.go | 4 +- plugins/actionscobra/plugin.go | 4 +- plugins/builtinprocessors/plugin.go | 46 ++- plugins/builtinprocessors/plugin_test.go | 83 ++++- plugins/verbosity/plugin.go | 2 +- plugins/yamldiscovery/plugin.go | 2 +- test/plugins/testactions/plugin.go | 2 +- .../runtime/container/image-build.txtar | 4 +- types.go | 5 + 32 files changed, 807 insertions(+), 515 deletions(-) create mode 100644 internal/launchr/services.go diff --git a/app.go b/app.go index b6800f6..f565eb8 100644 --- a/app.go +++ b/app.go @@ -2,10 +2,8 @@ package launchr import ( "errors" - "fmt" "io" "os" - "reflect" "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/action" @@ -24,7 +22,7 @@ type appImpl struct { // Services. streams Streams - services map[ServiceInfo]Service + services ServiceManager pluginMngr PluginManager } @@ -48,38 +46,14 @@ func (app *appImpl) SensitiveMask() *SensitiveMask { return launchr.GlobalSensit func (app *appImpl) RootCmd() *Command { return app.cmd } func (app *appImpl) CmdEarlyParsed() launchr.CmdEarlyParsed { return app.earlyCmd } +func (app *appImpl) Services() ServiceManager { return app.services } + func (app *appImpl) AddService(s Service) { - info := s.ServiceInfo() - launchr.InitServiceInfo(&info, s) - if _, ok := app.services[info]; ok { - panic(fmt.Errorf("service %s already exists, review your code", info)) - } - app.services[info] = s + app.services.Add(s) } func (app *appImpl) GetService(v any) { - // Check v is a pointer and implements [Service] to set a value later. - t := reflect.TypeOf(v) - isPtr := t != nil && t.Kind() == reflect.Pointer - var stype reflect.Type - if isPtr { - stype = t.Elem() - } - - // v must be [Service] but can't equal it because all elements implement it - // and the first value will always be returned. - intService := reflect.TypeOf((*Service)(nil)).Elem() - if !isPtr || !stype.Implements(intService) || stype == intService { - panic(fmt.Errorf("argument must be a pointer to a type (interface) implementing Service, %q given", t)) - } - for _, srv := range app.services { - st := reflect.TypeOf(srv) - if st.AssignableTo(stype) { - reflect.ValueOf(v).Elem().Set(reflect.ValueOf(srv)) - return - } - } - panic(fmt.Sprintf("service %q does not exist", stype)) + app.services.Get(v) } // init initializes application and plugins. @@ -123,19 +97,23 @@ func (app *appImpl) init() error { app.RegisterFS(action.NewDiscoveryFS(os.DirFS(actionsPath), app.GetWD())) // Prepare dependencies. - app.services = make(map[ServiceInfo]Service) + app.services = launchr.NewServiceManager() app.pluginMngr = launchr.NewPluginManagerWithRegistered() // @todo consider home dir for global config. config := launchr.ConfigFromFS(os.DirFS(app.cfgDir)) + actionProcs := action.NewTemplateProcessors() actionMngr := action.NewManager( action.WithDefaultRuntime(config), action.WithContainerRuntimeConfig(config, name+"_"), + action.WithServices(app.services), ) + actionMngr.SetTemplateProcessors(actionProcs) - // Register services for other modules. - app.AddService(actionMngr) - app.AddService(app.pluginMngr) - app.AddService(config) + // Register svcMngr for other modules. + app.services.Add(actionProcs) + app.services.Add(actionMngr) + app.services.Add(app.pluginMngr) + app.services.Add(config) Log().Debug("initialising application") diff --git a/docs/actions.md b/docs/actions.md index 81c6a69..59a5682 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -68,7 +68,7 @@ runtime: command: - python3 - {{ .myArg1 }} {{ .myArg2 }} - - {{ .optStr }} + - '{{ .optStr }}' - ${ENV_VAR} ``` diff --git a/docs/actions.schema.md b/docs/actions.schema.md index 7c8d25b..52df313 100644 --- a/docs/actions.schema.md +++ b/docs/actions.schema.md @@ -189,6 +189,8 @@ The action provides basic templating for all file based on arguments, options an For templating, the standard Go templating engine is used. Refer to [the library documentation](https://pkg.go.dev/text/template) for usage examples. +Since `action.yaml` must be valid YAML, wrap template strings in quotes to ensure proper parsing. + Arguments and Options are available by their machine names - `{{ .myArg1 }}`, `{{ .optStr }}`, `{{ .optArr }}`, etc. ### Predefined variables: @@ -227,85 +229,61 @@ runtime: image: {{ .optStr }}:latest command: - {{ .myArg1 }} {{ .MyArg2 }} - - {{ .optBool }} + - '{{ .optBool }}' ``` ### Available Command Template Functions -### `removeLine` -**Description:** A special template directive that removes the entire line from the final output. - -**Usage:** - -``` yaml -- "{{ if condition }}value{{ else }}{{ removeLine }}{{ end }}" -``` - ### `isNil` **Description:** Checks if a value is nil. **Usage:** -```yaml -- "{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ else }}{{ removeLine }}{{ end }}" +```gotemplate +{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ end }} ``` ### `isSet` **Description:** Checks if a value has been set (opposite of `isNil`). -```yaml -- "{{ if isSet .param_name }}--param={{ .param_name }}{{else}}{{ removeLine }}{{ end }}" +```gotemplate +{{ if isSet .param_name }}--param={{ .param_name }}{{ end }} ``` ### `isChanged` -**Description:** Checks if an option or argument value has been changed (dirty). +**Description:** Checks if an option or argument value has been changed (input by user). **Usage:** -```yaml -- '{{ if isChanged "param_name"}}--param={{.param_name}}{{else}}{{ removeLine }}{{ end }}' +```gotemplate +{{ if isChanged "param_name"}}--param={{.param_name}}{{ end }} ``` -### `removeLineIfNil` -**Description:** Removes the entire command line if the value is nil. +### `default` -**Usage:** - -```yaml -- "{{ removeLineIfNil .param_name }}" -``` - -### `removeLineIfSet` -**Description:** Removes the entire command line if the value is set (has no nil value). +**Description:** Returns a default value when the first parameter is `nil` or empty. +Emptiness is determined by its zero value - empty string `""`, integer `0`, structs with all zero-value fields, etc. +Or type implements `interface { IsEmpty() bool }`. **Usage:** - -```yaml -- "{{ removeLineIfSet .param_name }}" +```gotemplate +{{ .nil_value | default "foo" }} +{{ default .nil_value "bar" }} ``` -### `removeLineIfChanged` +### `config.Get` -**Description:** Removes the command line entry if the option/argument value has changed. +**Description:** Returns a [config](config.md) value by a path. **Usage:** - -``` yaml -- '{{ removeLineIfChanged "param_name" }}' -``` - -### `removeLineIfNotChanged` - -**Description:** Removes the command line entry if the option/argument value has not changed by the user. -Opposite of `removeLineIfChanged` - -**Usage:** - -``` yaml -- '{{ removeLineIfNotChanged "param_name" }}' +```gotemplate +{{ config.Get "foo.bar" }} # retrieves value of any type +{{ index (config.Get "foo.array-elem") 1 }} # retrieves specific array element +{{ config.Get "foo.null-elem" | default "foo" }} # uses default if value is nil +{{ config.Get "foo.missing-elem" | default "bar" }} # uses default if key doesn't exist ``` diff --git a/docs/development/plugin.md b/docs/development/plugin.md index 834d5c1..9240af9 100644 --- a/docs/development/plugin.md +++ b/docs/development/plugin.md @@ -169,7 +169,7 @@ Add the processor to the Action Manager: ```go var am action.Manager -app.GetService(&am) +app.Services().Get(&am) procReplace := GenericValueProcessor[procTestReplaceOptions]{ Types: []jsonschema.Type{jsonschema.String}, diff --git a/docs/development/service.md b/docs/development/service.md index 2adc7ed..0d8d352 100644 --- a/docs/development/service.md +++ b/docs/development/service.md @@ -25,7 +25,7 @@ import ( // Get a service from the App. func (p *Plugin) OnAppInit(app launchr.App) error { var cfg launchr.Config - app.GetService(&cfg) // Pass a pointer to init the value. + app.Services().Get(&cfg) // Pass a pointer to init the value. return nil } ``` @@ -41,7 +41,7 @@ import ( ) // Define a service and implement service interface. -// It is important to have a unique interface, the service is identified by it in launchr.GetService(). +// It is important to have a unique interface, the service is identified by it in [launchr.ServiceManager].Get(). type ExampleService interface { launchr.Service // Inherit launchr.Service // Provide other methods if needed. @@ -58,7 +58,7 @@ func (ex *exampleSrvImpl) ServiceInfo() launchr.ServiceInfo { // Register a service inside launchr. func (p *Plugin) OnAppInit(app launchr.App) error { srv := &exampleSrvImpl{} - app.AddService(srv) + app.Services().Add(srv) return nil } ``` diff --git a/example/actions/arguments/action.yaml b/example/actions/arguments/action.yaml index 2cbd1ad..d9e61ec 100644 --- a/example/actions/arguments/action.yaml +++ b/example/actions/arguments/action.yaml @@ -32,11 +32,7 @@ runtime: - /action/main.sh - "{{ .arg1 }}" - "{{ .arg2 }}" - - "{{ .firstoption|removeLineIfNil }}" - - "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{ else }}{{ removeLine }}{{ end }}" + - "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{else}}Second option is nil{{ end }}" - "{{ if isSet .thirdoption }}--thirdoption={{ .thirdoption }}{{else}}Third option is not set{{ end }}" - - "{{ removeLineIfSet .thirdoption }}" - - '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{else}}{{ removeLine }}{{ end }}' - - '{{ removeLineIfChanged "thirdoption" }}' - - '{{ if isChanged "thirdoption"}}Third Option is Changed{{else}}{{ removeLine }}{{ end }}' - - '{{ removeLineIfNotChanged "thirdoption" }}' + - '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{ end }}' + - '{{ if isChanged "thirdoption"}}Third Option is Changed{{ end }}' diff --git a/example/actions/platform/actions/build/action.yaml b/example/actions/platform/actions/build/action.yaml index 628991c..d2731ab 100644 --- a/example/actions/platform/actions/build/action.yaml +++ b/example/actions/platform/actions/build/action.yaml @@ -58,8 +58,16 @@ action: runtime: type: container -# image: python:3.7-slim image: ubuntu -# command: python3 {{ .opt4 }} - command: ["sh", "-c", "for i in $(seq 60); do if [ $((i % 2)) -eq 0 ]; then echo \"stdout: $$i\"; else echo \"stderr: $$i\" >&2; fi; sleep 1; done"] -# command: /bin/bash + command: + - "sh" + - "-c" + - | + for i in $(seq 60); do + if [ $((i % 2)) -eq 0 ]; then + echo "stdout: $$i"; + else + echo "stderr: $$i" >&2; + fi; + sleep 1; + done diff --git a/internal/launchr/config.go b/internal/launchr/config.go index fc2d73b..1997636 100644 --- a/internal/launchr/config.go +++ b/internal/launchr/config.go @@ -76,10 +76,19 @@ func (cfg *config) DirPath() string { return cfg.rootPath } -func (cfg *config) Exists(path string) bool { +func (cfg *config) exists(path string) bool { return cfg.koanf != nil && cfg.koanf.Exists(path) } +func (cfg *config) Exists(path string) bool { + var v any + err := cfg.Get(path, &v) + if err != nil { + return false + } + return cfg.exists(path) +} + func (cfg *config) Get(key string, v any) error { cfg.mx.Lock() defer cfg.mx.Unlock() @@ -100,7 +109,7 @@ func (cfg *config) Get(key string, v any) error { } } - ok = cfg.Exists(key) + ok = cfg.exists(key) if !ok { // Return default value. return nil diff --git a/internal/launchr/services.go b/internal/launchr/services.go new file mode 100644 index 0000000..1c2d87b --- /dev/null +++ b/internal/launchr/services.go @@ -0,0 +1,81 @@ +package launchr + +import ( + "fmt" + "reflect" +) + +// ServiceInfo provides service info for its initialization. +type ServiceInfo struct { + pkgPath string + typeName string +} + +func (s ServiceInfo) String() string { + return s.pkgPath + "." + s.typeName +} + +// Service is a common interface for a service to register. +type Service interface { + ServiceInfo() ServiceInfo +} + +// InitServiceInfo sets private fields for internal usage only. +func InitServiceInfo(si *ServiceInfo, s Service) { + si.pkgPath, si.typeName = GetTypePkgPathName(s) +} + +// ServiceManager is a basic Dependency Injection container storing registered [Service]. +type ServiceManager interface { + // Add registers a service. + // Panics if a service is not unique. + Add(s Service) + // Get retrieves a service of type [v] and assigns it to [v]. + // Panics if a service is not found. + Get(v any) +} + +type serviceManager struct { + services map[ServiceInfo]Service +} + +// NewServiceManager initializes ServiceManager. +func NewServiceManager() ServiceManager { + return &serviceManager{ + services: make(map[ServiceInfo]Service), + } +} + +func (sm *serviceManager) Add(s Service) { + info := s.ServiceInfo() + InitServiceInfo(&info, s) + if _, ok := sm.services[info]; ok { + panic(fmt.Errorf("service %s already exists, review your code", info)) + } + sm.services[info] = s +} + +func (sm *serviceManager) Get(v any) { + // Check v is a pointer and implements [Service] to set a value later. + t := reflect.TypeOf(v) + isPtr := t != nil && t.Kind() == reflect.Pointer + var stype reflect.Type + if isPtr { + stype = t.Elem() + } + + // v must be [Service] but can't equal it because all elements implement it + // and the first value will always be returned. + intService := reflect.TypeOf((*Service)(nil)).Elem() + if !isPtr || !stype.Implements(intService) || stype == intService { + panic(fmt.Errorf("argument must be a pointer to a type (interface) implementing Service, %q given", t)) + } + for _, srv := range sm.services { + st := reflect.TypeOf(srv) + if st.AssignableTo(stype) { + reflect.ValueOf(v).Elem().Set(reflect.ValueOf(srv)) + return + } + } + panic(fmt.Sprintf("service %q does not exist", stype)) +} diff --git a/internal/launchr/types.go b/internal/launchr/types.go index a851666..154eebe 100644 --- a/internal/launchr/types.go +++ b/internal/launchr/types.go @@ -42,10 +42,14 @@ type App interface { SetStreams(s Streams) // AddService registers a service in the app. // Panics if a service is not unique. + // Deprecated: use app.Services().Add(s) AddService(s Service) // GetService retrieves a service of type [v] and assigns it to [v]. // Panics if a service is not found. + // Deprecated: use app.Services().Get(&v) GetService(v any) + // Services returns a service manager. + Services() ServiceManager // SensitiveWriter wraps given writer with a sensitive mask. SensitiveWriter(w io.Writer) io.Writer // SensitiveMask returns current sensitive mask to add values to mask. @@ -203,26 +207,6 @@ type pluginManagerMap PluginsMap func (m pluginManagerMap) ServiceInfo() ServiceInfo { return ServiceInfo{} } func (m pluginManagerMap) All() PluginsMap { return m } -// ServiceInfo provides service info for its initialization. -type ServiceInfo struct { - pkgPath string - typeName string -} - -func (s ServiceInfo) String() string { - return s.pkgPath + "." + s.typeName -} - -// Service is a common interface for a service to register. -type Service interface { - ServiceInfo() ServiceInfo -} - -// InitServiceInfo sets private fields for internal usage only. -func InitServiceInfo(si *ServiceInfo, s Service) { - si.pkgPath, si.typeName = GetTypePkgPathName(s) -} - // ManagedFS is a common interface for FS registered in launchr. type ManagedFS interface { fs.FS diff --git a/pkg/action/action.go b/pkg/action/action.go index 6a2fde7..f887acf 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -20,6 +20,8 @@ type Action struct { // loader is a function to load action definition. // Helpful to reload with replaced variables. loader Loader + // services is a service manager to help with Dependency Injection. + services launchr.ServiceManager // wd is a working directory set from app level. // Usually current working directory, but may be overridden by a plugin. wd string @@ -52,7 +54,18 @@ func New(idp IDProvider, l Loader, fsys DiscoveryFS, fpath string) *Action { // NewFromYAML creates a new action from yaml content. func NewFromYAML(id string, b []byte) *Action { - return New(StringID(id), &YamlLoader{Bytes: b}, NewDiscoveryFS(nil, ""), "") + return New( + StringID(id), + &YamlLoader{ + Bytes: b, + Processor: NewPipeProcessor( + envProcessor{}, + inputProcessor{}, + ), + }, + NewDiscoveryFS(nil, ""), + "", + ) } // NewYAMLFromFS creates an action from the given filesystem. @@ -93,8 +106,13 @@ func (a *Action) Clone() *Action { return c } -// SetProcessors sets the value processors for an [Action]. -func (a *Action) SetProcessors(list map[string]ValueProcessor) error { +// SetServices sets a [launchr.ServiceManager] for Dependency Injection. +func (a *Action) SetServices(s launchr.ServiceManager) { + a.services = s +} + +// setProcessors sets the value processors for an [Action]. +func (a *Action) setProcessors(list map[string]ValueProcessor) error { def := a.ActionDef() for _, params := range []ParametersList{def.Arguments, def.Options} { for _, p := range params { @@ -195,7 +213,7 @@ func (a *Action) DefinitionEncoded() ([]byte, error) { return a.loader.Content() func (a *Action) Raw() (*Definition, error) { var err error if a.defRaw == nil { - a.defRaw, err = a.loader.LoadRaw() + a.defRaw, err = a.loader.Load(nil) } return a.defRaw, err } @@ -211,7 +229,7 @@ func (a *Action) EnsureLoaded() (err error) { return err } // Load with replacements. - a.def, err = a.loader.Load(LoadContext{Action: a}) + a.def, err = a.loader.Load(&LoadContext{Action: a, Services: a.services}) return err } diff --git a/pkg/action/action.input.go b/pkg/action/action.input.go index 20fde00..3183e16 100644 --- a/pkg/action/action.input.go +++ b/pkg/action/action.input.go @@ -205,6 +205,61 @@ func (input *Input) Streams() launchr.Streams { return input.io } +func (input *Input) execValueProcessors() (err error) { + // TODO: Maybe it must run on value change. Need to review how we propagate errors. + def := input.action.ActionDef() + // Process arguments. + err = processInputParams(def.Arguments, input.Args(), input.ArgsChanged(), input) + if err != nil { + return err + } + + // Process options. + err = processInputParams(def.Options, input.Opts(), input.OptsChanged(), input) + if err != nil { + return err + } + + return nil +} + +// processInputParams applies value processors to input parameters. +func processInputParams(def ParametersList, inp InputParams, changed InputParams, input *Input) error { + var err error + for _, p := range def { + _, isChanged := changed[p.Name] + res := inp[p.Name] + for i, procDef := range p.Process { + handler := p.processors[i] + res, err = handler(res, ValueProcessorContext{ + ValOrig: inp[p.Name], + IsChanged: isChanged, + Input: input, + DefParam: p, + Action: input.action, + }) + if err != nil { + return ErrValueProcessorHandler{ + Processor: procDef.ID, + Param: p.Name, + Err: err, + } + } + } + // Cast to []any slice because jsonschema validator supports only this type. + if p.Type == jsonschema.Array { + res = CastSliceTypedToAny(res) + } + // If the value was changed, we can safely override the value. + // If the value was not changed and processed is nil, do not add it. + if isChanged || res != nil { + inp[p.Name] = res + } + } + + return nil +} + func argsNamedToPos(args InputParams, argsDef ParametersList) []string { if args == nil { return nil diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index ad575b9..96d1ca0 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -65,13 +65,16 @@ func Test_Action(t *testing.T) { // Test templating in executable. envVar1 := "envval1" _ = os.Setenv("TEST_ENV_1", envVar1) + defer func() { + _ = os.Unsetenv("TEST_ENV_1") + }() execExp := []string{ "/bin/sh", "-c", "ls -lah", fmt.Sprintf("%v %v %v %v", inputArgs["arg2"], inputArgs["arg1"], inputArgs["arg-1"], inputArgs["arg_12"]), fmt.Sprintf("%v %v %v %v %v %v", inputOpts["opt3"], inputOpts["opt2"], inputOpts["opt1"], inputOpts["opt-1"], inputOpts["opt4"], inputOpts["optarr"]), - fmt.Sprintf("%v", envVar1), + fmt.Sprintf("%v ", envVar1), fmt.Sprintf("%v ", envVar1), } act.Reset() @@ -127,6 +130,26 @@ func Test_Action_NewYAMLFromFS(t *testing.T) { assert.NoFileExists(t, fpath) } +func Test_ActionMultiline(t *testing.T) { + t.Parallel() + a := NewFromYAML("multiline_test", []byte(validMultilineYaml)) + + envvar1 := "foo\nbar\nbaz" + _ = os.Setenv("TEST_MULTILINE_ENV1", envvar1) + defer func() { + _ = os.Unsetenv("TEST_MULTILINE_ENV1") + }() + input := NewInput(a, nil, nil, nil) + require.NotNil(t, input) + input.SetValidated(true) + require.NoError(t, a.SetInput(input)) + + rdef := a.RuntimeDef() + assert.Contains(t, rdef.Container.Env, "MY_MULTILINE_ENV1="+envvar1) + assert.Contains(t, rdef.Container.Env, "MY_MULTILINE_ENV2="+envvar1) + assert.Contains(t, rdef.Container.Env, "MY_MULTILINE_ENV3="+envvar1+"\n") +} + func Test_ActionInput(t *testing.T) { t.Parallel() assert := assert.New(t) diff --git a/pkg/action/loader.go b/pkg/action/loader.go index 6303799..48c5288 100644 --- a/pkg/action/loader.go +++ b/pkg/action/loader.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "os" + "reflect" "regexp" "strings" "text/template" @@ -11,29 +12,54 @@ import ( "github.com/launchrctl/launchr/internal/launchr" ) -const tokenRmLine = "" //nolint:gosec // G101: Not a credential. - -var rgxTokenRmLine = regexp.MustCompile(`.*` + tokenRmLine + `.*\n?`) - // Loader is an interface for loading an action file. type Loader interface { // Content returns the raw file content. Content() ([]byte, error) // Load parses Content to a Definition with substituted values. - Load(LoadContext) (*Definition, error) - // LoadRaw parses Content to a Definition raw values. Template strings are escaped. - LoadRaw() (*Definition, error) + Load(*LoadContext) (*Definition, error) } // LoadContext stores relevant and isolated data needed for processors. type LoadContext struct { - Action *Action + Action *Action + Services launchr.ServiceManager + + tplVars map[string]any + tplFuncMap template.FuncMap +} + +func (ctx *LoadContext) getActionTemplateProcessors() TemplateProcessors { + if ctx.Services == nil { + return NewTemplateProcessors() + } + var tp TemplateProcessors + ctx.Services.Get(&tp) + return tp +} + +func (ctx *LoadContext) getTemplateFuncMap() template.FuncMap { + if ctx.tplFuncMap == nil { + procs := ctx.getActionTemplateProcessors() + ctx.tplFuncMap = procs.GetTemplateFuncMap(TemplateFuncContext{Action: ctx.Action}) + } + return ctx.tplFuncMap +} + +func (ctx *LoadContext) getTemplateData() map[string]any { + if ctx.tplVars == nil { + def := ctx.Action.ActionDef() + // Collect template variables. + ctx.tplVars = convertInputToTplVars(ctx.Action.Input(), def) + addPredefinedVariables(ctx.tplVars, ctx.Action) + } + return ctx.tplVars } // LoadProcessor is an interface for processing input on load. type LoadProcessor interface { // Process gets an input action file data and returns a processed result. - Process(LoadContext, []byte) ([]byte, error) + Process(*LoadContext, string) (string, error) } type pipeProcessor struct { @@ -45,20 +71,26 @@ func NewPipeProcessor(p ...LoadProcessor) LoadProcessor { return &pipeProcessor{p: p} } -func (p *pipeProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { +func (p *pipeProcessor) Process(ctx *LoadContext, s string) (string, error) { var err error for _, proc := range p.p { - b, err = proc.Process(ctx, b) + s, err = proc.Process(ctx, s) if err != nil { - return b, err + return s, err } } - return b, nil + return s, nil } type envProcessor struct{} -func (p envProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { +func (p envProcessor) Process(ctx *LoadContext, s string) (string, error) { + if ctx.Action == nil { + panic("envProcessor received nil LoadContext.Action") + } + if !strings.Contains(s, "$") { + return s, nil + } pv := newPredefinedVars(ctx.Action) getenv := func(key string) string { v, ok := pv.getenv(key) @@ -67,8 +99,7 @@ func (p envProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { } return launchr.Getenv(key) } - s := os.Expand(string(b), getenv) - return []byte(s), nil + return os.Expand(s, getenv), nil } type inputProcessor struct{} @@ -88,126 +119,46 @@ func (err errMissingVar) Error() string { return fmt.Sprintf("the following variables were used but never defined: %v", f) } -// actionTplFuncs defined template functions available during parsing of an action yaml. -func actionTplFuncs(input *Input) template.FuncMap { - // Helper function to get value by name from args or opts - getValue := func(name string) any { - args := input.Args() - if arg, ok := args[name]; ok { - return arg - } - - opts := input.Opts() - if opt, ok := opts[name]; ok { - return opt - } - - return nil +func (p inputProcessor) Process(ctx *LoadContext, s string) (string, error) { + if ctx.Action == nil { + panic("inputProcessor received nil LoadContext.Action") } - - // Helper function to check if a parameter is changed - isParamChanged := func(name string) bool { - return input.IsOptChanged(name) || input.IsArgChanged(name) - } - - return template.FuncMap{ - // Checks if a value is nil. Used in conditions. - "isNil": func(v any) bool { - return v == nil - }, - // Checks if a value is not nil. Used in conditions. - "isSet": func(v any) bool { - return v != nil - }, - // Checks if a value is changed. Used in conditions. - "isChanged": func(v any) bool { - name, ok := v.(string) - if !ok { - return false - } - - return isParamChanged(name) - }, - // Removes a line if a given value is nil or pass through. - "removeLineIfNil": func(v any) any { - if v == nil { - return tokenRmLine - } - return v - }, - // Removes a line if a given value is not nil or pass through. - "removeLineIfSet": func(v any) any { - if v != nil { - return tokenRmLine - } - - return v - }, - // Removes a line if a given value is changed or pass through. - "removeLineIfChanged": func(name string) any { - if isParamChanged(name) { - return tokenRmLine - } - - return getValue(name) - }, - // Removes a line if a given value is not changed or pass through. - "removeLineIfNotChanged": func(name string) any { - if !isParamChanged(name) { - return tokenRmLine - } - - return getValue(name) - }, - // Removes current line. - "removeLine": func() string { - return tokenRmLine - }, + if !strings.Contains(s, "{{") { + return s, nil } -} -func (p inputProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { - if ctx.Action == nil { - return b, nil - } - a := ctx.Action - def := ctx.Action.ActionDef() // Collect template variables. - data := ConvertInputToTplVars(a.Input(), def) - addPredefinedVariables(data, a) + data := ctx.getTemplateData() - // Parse action without variables to validate - tpl := template.New(a.ID).Funcs(actionTplFuncs(a.Input())) + // Parse action yaml. + tpl := template.New(s).Funcs(ctx.getTemplateFuncMap()) + _, err := tpl.Parse(s) - _, err := tpl.Parse(string(b)) // Check if variables have dashes to show the error properly. err = checkDashErr(err, data) if err != nil { - return nil, err + return s, err } // Execute template. - buf := bytes.NewBuffer(make([]byte, 0, len(b))) + buf := bytes.NewBuffer(make([]byte, 0, len(s))) err = tpl.Execute(buf, data) if err != nil { - return nil, err + return s, err } // Find if some vars were used but not defined in arguments or options. - res := buf.Bytes() - err = findMissingVars(b, res, data) + res := buf.String() + err = findMissingVars(s, res, data) if err != nil { - return nil, err + return s, err } - // Remove all lines containing [tokenRmLine]. - res = rgxTokenRmLine.ReplaceAll(res, []byte("")) - return res, nil } -// ConvertInputToTplVars creates a map with input variables suitable for template engine. -func ConvertInputToTplVars(input *Input, ac *DefAction) map[string]any { +// convertInputToTplVars creates a map with input variables suitable for template engine. +func convertInputToTplVars(input *Input, ac *DefAction) map[string]any { args := input.Args() opts := input.Opts() values := make(map[string]any, len(args)+len(opts)) @@ -264,14 +215,14 @@ Action definition is correct, but dashes are not allowed in templates, replace " return err } -func findMissingVars(orig, repl []byte, data map[string]any) error { +func findMissingVars(orig, repl string, data map[string]any) error { miss := make(map[string]struct{}) - if !bytes.Contains(repl, []byte("")) { + if !strings.Contains(repl, "") { return nil } - matches := rgxTplVar.FindAllSubmatch(orig, -1) + matches := rgxTplVar.FindAllStringSubmatch(orig, -1) for _, m := range matches { - k := string(m[1]) + k := m[1] if _, ok := data[k]; !ok { miss[k] = struct{}{} } @@ -283,3 +234,140 @@ func findMissingVars(orig, repl []byte, data map[string]any) error { } return nil } + +// processStructStringsInPlace walks over a struct recursively and processes string fields in-place using the provided processor function. +func processStructStringsInPlace(v any, processor func(string) (string, error)) error { + return processStructValueInPlace(reflect.ValueOf(v), processor) +} + +func processStructValueInPlace(v reflect.Value, processor func(string) (string, error)) error { + // Handle pointers and interfaces + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + return processStructValueInPlace(v.Elem(), processor) + } + + if v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + return processStructValueInPlace(v.Elem(), processor) + } + + switch v.Kind() { + case reflect.String: + return processStringValueInPlace(v, processor) + case reflect.Struct: + return processStructFieldsInPlace(v, processor) + case reflect.Slice, reflect.Array: + return processSliceOrArrayInPlace(v, processor) + case reflect.Map: + return processMapValueInPlace(v, processor) + default: + return nil + } +} + +func processStringValueInPlace(v reflect.Value, processor func(string) (string, error)) error { + if !v.CanSet() { + return nil + } + + processed, err := processor(v.String()) + if err != nil { + return err + } + + // Only process basic string types directly + if v.Type() == reflect.TypeOf("") { + v.SetString(processed) + return nil + } + + // For custom string types, try to convert and set + newVal := reflect.ValueOf(processed) + if newVal.Type().ConvertibleTo(v.Type()) { + v.Set(newVal.Convert(v.Type())) + } + + return nil +} + +func processStructFieldsInPlace(v reflect.Value, processor func(string) (string, error)) error { + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + structField := v.Type().Field(i) + + // Skip unexported fields + if !structField.IsExported() { + continue + } + + if field.CanInterface() { + if err := processStructValueInPlace(field, processor); err != nil { + return err + } + } + } + + return nil +} + +func processSliceOrArrayInPlace(v reflect.Value, processor func(string) (string, error)) error { + for i := 0; i < v.Len(); i++ { + if err := processStructValueInPlace(v.Index(i), processor); err != nil { + return err + } + } + + return nil +} + +func processMapValueInPlace(v reflect.Value, processor func(string) (string, error)) error { + // For maps, we need to handle key processing differently since map keys are not addressable + keysToUpdate := make(map[reflect.Value]reflect.Value) + + for _, key := range v.MapKeys() { + value := v.MapIndex(key) + + // Process the value in-place if possible + if err := processStructValueInPlace(value, processor); err != nil { + return err + } + + // Check if key needs processing (only for string keys) + if key.Kind() == reflect.String { + processed, err := processor(key.String()) + if err != nil { + return err + } + + // If the key changed, we need to update the map + if processed != key.String() { + var newKey reflect.Value + if key.Type() == reflect.TypeOf("") { + newKey = reflect.ValueOf(processed) + } else { + newKeyVal := reflect.ValueOf(processed) + if newKeyVal.Type().ConvertibleTo(key.Type()) { + newKey = newKeyVal.Convert(key.Type()) + } else { + continue // Skip if not convertible + } + } + keysToUpdate[key] = newKey + } + } + } + + // Update map keys that changed + for oldKey, newKey := range keysToUpdate { + value := v.MapIndex(oldKey) + v.SetMapIndex(oldKey, reflect.Value{}) // Delete old key + v.SetMapIndex(newKey, value) // Set with new key + } + + return nil +} diff --git a/pkg/action/loader_test.go b/pkg/action/loader_test.go index 4555171..1bac1b2 100644 --- a/pkg/action/loader_test.go +++ b/pkg/action/loader_test.go @@ -3,7 +3,6 @@ package action import ( "fmt" "os" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -40,18 +39,18 @@ func Test_EnvProcessor(t *testing.T) { act := testLoaderAction() proc := envProcessor{} defer func() { - _ = os.Unsetenv("TEST_ENV1") - _ = os.Unsetenv("TEST_ENV2") + _ = os.Unsetenv("TEST_ENVPROC1") + _ = os.Unsetenv("TEST_ENVPROC2") }() - _ = os.Setenv("TEST_ENV1", "VAL1") - _ = os.Setenv("TEST_ENV2", "VAL2") - s := "$TEST_ENV1$TEST_ENV1,${TEST_ENV2},$$TEST_ENV1,${TEST_ENV_UNDEF},${TEST_ENV_UNDEF-$TEST_ENV1},${TEST_ENV_UNDEF:-$TEST_ENV2},${TEST_ENV2+$TEST_ENV1},${TEST_ENV1:+$TEST_ENV2}" - res, err := proc.Process(LoadContext{Action: act}, []byte(s)) + _ = os.Setenv("TEST_ENVPROC1", "VAL1") + _ = os.Setenv("TEST_ENVPROC2", "VAL2") + s := "$TEST_ENVPROC1$TEST_ENVPROC1,${TEST_ENVPROC2},$$TEST_ENVPROC1,${TEST_ENVPROC_UNDEF},${TEST_ENVPROC_UNDEF-$TEST_ENVPROC1},${TEST_ENVPROC_UNDEF:-$TEST_ENVPROC2},${TEST_ENVPROC2+$TEST_ENVPROC1},${TEST_ENVPROC1:+$TEST_ENVPROC2}" + res, err := proc.Process(&LoadContext{Action: act}, s) assert.NoError(t, err) - assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENV1,,VAL1,VAL2,VAL1,VAL2", string(res)) + assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENVPROC1,,VAL1,VAL2,VAL1,VAL2", string(res)) // Test action predefined env variables. s = "$CBIN,$ACTION_ID,$ACTION_WD,$ACTION_DIR,$DISCOVERY_DIR" - res, err = proc.Process(LoadContext{Action: act}, []byte(s)) + res, err = proc.Process(&LoadContext{Action: act}, s) exp := fmt.Sprintf("%s,%s,%s,%s,%s", launchr.Executable(), act.ID, act.WorkDir(), act.Dir(), act.fs.Realpath()) assert.NoError(t, err) assert.Equal(t, exp, string(res)) @@ -60,7 +59,7 @@ func Test_EnvProcessor(t *testing.T) { func Test_InputProcessor(t *testing.T) { t.Parallel() act := testLoaderAction() - ctx := LoadContext{Action: act} + ctx := &LoadContext{Action: act} proc := inputProcessor{} input := NewInput(act, InputParams{"arg1": "arg1"}, InputParams{"optStr": "optVal1", "opt-str": "opt-val2"}, nil) input.SetValidated(true) @@ -69,99 +68,63 @@ func Test_InputProcessor(t *testing.T) { // Check all available variables are replaced. s := "{{ .arg1 }},{{ .optStr }},{{ .opt_str }}" - res, err := proc.Process(ctx, []byte(s)) + res, err := proc.Process(ctx, s) assert.NoError(t, err) - assert.Equal(t, "arg1,optVal1,opt-val2", string(res)) + assert.Equal(t, "arg1,optVal1,opt-val2", res) // Check the variable has incorrect name and correct error is returned. s = "{{ .opt-str }}" - res, err = proc.Process(ctx, []byte(s)) + res, err = proc.Process(ctx, s) assert.ErrorContains(t, err, "unexpected '-' symbol in a template variable.") - assert.Equal(t, "", string(res)) + assert.Equal(t, s, res) // Check that we have an error when missing variables are not handled. errMissVars := errMissingVar{vars: map[string]struct{}{"optUnd": {}, "arg2": {}}} s = "{{ .arg2 }},{{ .optUnd }}" - res, err = proc.Process(ctx, []byte(s)) + res, err = proc.Process(ctx, s) assert.Equal(t, errMissVars, err) - assert.Equal(t, "", string(res)) + assert.Equal(t, s, res) // Remove line if a variable not exists or is nil. - s = `- "{{ .arg1 | removeLineIfNil }}" -- "{{ .optUnd | removeLineIfNil }}" # Piping with new line -- "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}" -- "{{ if (isNil .optUnd) }}{{ removeLine }}{{ end }}" # Function call without new line` - res, err = proc.Process(ctx, []byte(s)) + s = `- "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}" +- "{{ if (isNil .optUnd) }}optUnd{{ end }}"` + res, err = proc.Process(ctx, s) assert.NoError(t, err) - assert.Equal(t, "- \"arg1\"\n- \"arg1 is not nil\"\n", string(res)) + assert.Equal(t, "- \"arg1 is not nil\"\n- \"optUnd\"", res) // Remove line if a variable not exists or is nil, 1 argument is not defined and not checked. - s = `- "{{ .arg1 | removeLineIfNil }}" -- "{{ .optUnd|removeLineIfNil }}" # Piping with new line -- "{{ .arg2 }}" + s = `- "{{ .arg2 }}" - "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}" -- "{{ if (isNil .optUnd) }}{{ removeLine }}{{ end }}" # Function call without new line` - _, err = proc.Process(ctx, []byte(s)) +- "{{ if (isNil .optUnd) }}optUnd{{ end }}"` + _, err = proc.Process(ctx, s) assert.Equal(t, errMissVars, err) - s = `- "{{ if isSet .arg1 }}arg1 is set"{{end}} -- "{{ removeLineIfSet .arg1 }}" # Function call without new line -- "{{ if isChanged .arg1 }}arg1 is changed{{end}}" -- '{{ removeLineIfNotChanged "arg1" }}' -- '{{ removeLineIfChanged "arg1" }}' # Function call without new line` - res, err = proc.Process(ctx, []byte(s)) + s = `- "{{ if isSet .arg1 }}arg1 is set{{end}}" +- "{{ if isChanged .arg1 }}arg1 is changed{{end}}"` + res, err = proc.Process(ctx, s) assert.NoError(t, err) - assert.Equal(t, "- \"arg1 is set\"\n- \"arg1 is changed\"\n- 'arg1'\n", string(res)) -} - -func Test_YamlTplCommentsProcessor(t *testing.T) { - t.Parallel() - act := testLoaderAction() - ctx := LoadContext{Action: act} - proc := NewPipeProcessor( - escapeYamlTplCommentsProcessor{}, - inputProcessor{}, - ) - input := NewInput(act, InputParams{"arg1": "arg1"}, InputParams{"optStr": "optVal1"}, nil) - input.SetValidated(true) - err := act.SetInput(input) - require.NoError(t, err) - // Check the commented strings are not considered. - s := ` -t: "{{ .arg1 }} # {{ .optStr }}" -t: '{{ .arg1 }} # {{ .optStr }}' -t: {{ .arg1 }} # {{ .optUnd }} -# {{ .optUnd }} {{ .arg1 }} - ` - res, err := proc.Process(ctx, []byte(s)) - require.NoError(t, err) - assert.Equal(t, "t: \"arg1 # optVal1\"\nt: 'arg1 # optVal1'\nt: arg1", strings.TrimSpace(string(res))) - s = `t: "{{ .arg1 }} # {{ .optUnd }}""` - // Check we still have an error on an undefined variable. - res, err = proc.Process(ctx, []byte(s)) - assert.Equal(t, err, errMissingVar{vars: map[string]struct{}{"optUnd": {}}}) - assert.Equal(t, "", string(res)) + assert.Equal(t, "- \"arg1 is set\"\n- \"arg1 is changed\"", res) } func Test_PipeProcessor(t *testing.T) { t.Parallel() act := testLoaderAction() - ctx := LoadContext{Action: act} + ctx := &LoadContext{Action: act} proc := NewPipeProcessor( envProcessor{}, inputProcessor{}, ) - _ = os.Setenv("TEST_ENV1", "VAL1") + _ = os.Setenv("TEST_ENVPROC3", "VAL1") defer func() { - _ = os.Unsetenv("TEST_ENV1") + _ = os.Unsetenv("TEST_ENVPROC3") }() input := NewInput(act, InputParams{"arg1": "arg1"}, InputParams{"optStr": "optVal1"}, nil) input.SetValidated(true) err := act.SetInput(input) require.NoError(t, err) - s := "$TEST_ENV1,{{ .arg1 }},{{ .optStr }}" - res, err := proc.Process(ctx, []byte(s)) + s := "$TEST_ENVPROC3,{{ .arg1 }},{{ .optStr }}" + res, err := proc.Process(ctx, s) require.NoError(t, err) - assert.Equal(t, "VAL1,arg1,optVal1", string(res)) + assert.Equal(t, "VAL1,arg1,optVal1", res) } diff --git a/pkg/action/manager.go b/pkg/action/manager.go index 74d04cf..cca66b4 100644 --- a/pkg/action/manager.go +++ b/pkg/action/manager.go @@ -11,7 +11,6 @@ import ( "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/driver" - "github.com/launchrctl/launchr/pkg/jsonschema" ) // DiscoverActionsFn defines a function to discover actions. @@ -48,10 +47,12 @@ type Manager interface { // current state. GetPersistentFlags() *FlagsGroup - // AddValueProcessor adds processor to list of available processors - AddValueProcessor(name string, vp ValueProcessor) - // GetValueProcessors returns list of available processors - GetValueProcessors() map[string]ValueProcessor + // TemplateProcessors for backward-compatibility. + // Deprecated: Use [TemplateProcessors] service directly. + TemplateProcessors + // SetTemplateProcessors sets [TemplateProcessors] for backward-compatibility. + // Deprecated: no replacement. + SetTemplateProcessors(TemplateProcessors) // AddDiscovery registers a discovery callback to find actions. AddDiscovery(DiscoverActionsFn) @@ -100,7 +101,6 @@ type actionManagerMap struct { actionAliases map[string]string mx sync.Mutex dwFns []DecorateWithFn - processors map[string]ValueProcessor idProvider IDProvider // Actions discovery. @@ -111,6 +111,7 @@ type actionManagerMap struct { persistentFlags *FlagsGroup runManagerMap + TemplateProcessors // TODO: It's here to support the interface. Refactor when the interface is updated. } // NewManager constructs a new action manager. @@ -119,7 +120,6 @@ func NewManager(withFns ...DecorateWithFn) Manager { actionStore: make(map[string]*Action), actionAliases: make(map[string]string), dwFns: withFns, - processors: make(map[string]ValueProcessor), persistentFlags: NewFlagsGroup(jsonschemaPropPersistent), @@ -156,7 +156,7 @@ func (m *actionManagerMap) add(a *Action) error { m.actionAliases[alias] = a.ID } // Set action related processors. - err = a.SetProcessors(m.GetValueProcessors()) + err = a.setProcessors(m.GetValueProcessors()) if err != nil { // Skip action because the definition is not correct. return err @@ -292,15 +292,8 @@ func (m *actionManagerMap) callDiscoveryFn(ctx context.Context, fn DiscoverActio return nil } -func (m *actionManagerMap) AddValueProcessor(name string, vp ValueProcessor) { - if _, ok := m.processors[name]; ok { - panic(fmt.Sprintf("processor `%q` with the same name already exists", name)) - } - m.processors[name] = vp -} - -func (m *actionManagerMap) GetValueProcessors() map[string]ValueProcessor { - return m.processors +func (m *actionManagerMap) SetTemplateProcessors(tp TemplateProcessors) { + m.TemplateProcessors = tp } func (m *actionManagerMap) AddDecorators(withFns ...DecorateWithFn) { @@ -381,16 +374,7 @@ func (m *actionManagerMap) ValidateInput(a *Action, input *Input) error { return err } - def := a.ActionDef() - - // Process arguments. - err = m.processInputParams(def.Arguments, input.Args(), input.ArgsChanged(), input) - if err != nil { - return err - } - - // Process options. - err = m.processInputParams(def.Options, input.Opts(), input.OptsChanged(), input) + err = input.execValueProcessors() if err != nil { return err } @@ -409,44 +393,6 @@ func (m *actionManagerMap) ValidateInput(a *Action, input *Input) error { return nil } -// processInputParams applies value processors to input parameters. -func (m *actionManagerMap) processInputParams(def ParametersList, inp InputParams, changed InputParams, input *Input) error { - // @todo move to a separate service with full input validation. See notes in actionManagerMap.ValidateFlags. - var err error - for _, p := range def { - _, isChanged := changed[p.Name] - res := inp[p.Name] - for i, procDef := range p.Process { - handler := p.processors[i] - res, err = handler(res, ValueProcessorContext{ - ValOrig: inp[p.Name], - IsChanged: isChanged, - Input: input, - DefParam: p, - Action: input.action, - }) - if err != nil { - return ErrValueProcessorHandler{ - Processor: procDef.ID, - Param: p.Name, - Err: err, - } - } - } - // Cast to []any slice because jsonschema validator supports only this type. - if p.Type == jsonschema.Array { - res = CastSliceTypedToAny(res) - } - // If the value was changed, we can safely override the value. - // If the value was not changed and processed is nil, do not add it. - if isChanged || res != nil { - inp[p.Name] = res - } - } - - return nil -} - // RunInfo stores information about a running action. type RunInfo struct { ID string @@ -578,3 +524,10 @@ func WithContainerRuntimeConfig(cfg launchr.Config, prefix string) DecorateWithF } } } + +// WithServices add global app to action. Helps with DI in actions. +func WithServices(services launchr.ServiceManager) DecorateWithFn { + return func(_ Manager, a *Action) { + a.SetServices(services) + } +} diff --git a/pkg/action/process.go b/pkg/action/process.go index 1b48959..8ab1722 100644 --- a/pkg/action/process.go +++ b/pkg/action/process.go @@ -5,10 +5,127 @@ import ( "reflect" "slices" "strings" + "text/template" + "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/jsonschema" ) +// TemplateProcessors handles template processors used on an action load. +type TemplateProcessors interface { + launchr.Service + // AddValueProcessor adds processor to list of available processors + AddValueProcessor(name string, vp ValueProcessor) + // GetValueProcessors returns list of available processors + GetValueProcessors() map[string]ValueProcessor + AddTemplateFunc(name string, fn any) + GetTemplateFuncMap(ctx TemplateFuncContext) template.FuncMap +} + +// TemplateFuncContext stores context used for processing. +type TemplateFuncContext struct { + Action *Action +} + +type processManager struct { + vproc map[string]ValueProcessor + tplFns map[string]any +} + +// NewTemplateProcessors initializes TemplateProcessors with default functions. +func NewTemplateProcessors() TemplateProcessors { + p := &processManager{ + vproc: make(map[string]ValueProcessor), + tplFns: make(map[string]any), + } + defaultTemplateFunc(p) + return p +} + +// defaultTemplateFunc defines template functions available during parsing of an action yaml. +func defaultTemplateFunc(p *processManager) { + // Returns a default value if v is nil or zero. + p.AddTemplateFunc("default", func(v, d any) any { + // Check IsEmpty method. + if v, ok := v.(interface{ IsEmpty() bool }); ok && v.IsEmpty() { + return d + } + + // Check zero value, for example, empty string, 0, false, + // or in the case of structs that all fields are zero values. + if reflect.ValueOf(v).IsZero() { + return d + } + + // Checks if value is nil. + if v == nil { + return d + } + + return v + }) + + // Checks if a value is nil. Used in conditions. + p.AddTemplateFunc("isNil", func(v any) bool { + return v == nil + }) + + // Checks if a value is not nil. Used in conditions. + p.AddTemplateFunc("isSet", func(v any) bool { + return v != nil + }) + + // Checks if a value is changed. Used in conditions. + p.AddTemplateFunc("isChanged", func(ctx TemplateFuncContext) any { + return func(v any) bool { + name, ok := v.(string) + if !ok { + return false + } + input := ctx.Action.Input() + return input.IsOptChanged(name) || input.IsArgChanged(name) + } + }) +} + +func (m *processManager) ServiceInfo() launchr.ServiceInfo { + return launchr.ServiceInfo{} +} + +func (m *processManager) AddValueProcessor(name string, vp ValueProcessor) { + if _, ok := m.vproc[name]; ok { + panic(fmt.Sprintf("value processor %q with the same name already exists", name)) + } + m.vproc[name] = vp +} + +func (m *processManager) GetValueProcessors() map[string]ValueProcessor { + return m.vproc +} + +func (m *processManager) AddTemplateFunc(name string, fn any) { + if _, ok := m.tplFns[name]; ok { + panic(fmt.Sprintf("template function %q with the same name already exists", name)) + } + m.tplFns[name] = fn +} + +func (m *processManager) GetTemplateFuncMap(ctx TemplateFuncContext) template.FuncMap { + tplFuncMap := template.FuncMap{} + for k, v := range m.tplFns { + switch v := v.(type) { + case func(ctx TemplateFuncContext) any: + // Template function with action context. + tplFuncMap[k] = v(ctx) + + default: + // Usual template function processor. + tplFuncMap[k] = v + } + } + return tplFuncMap +} + // ValueProcessor defines an interface for processing a value based on its type and some options. type ValueProcessor interface { IsApplicable(valueType jsonschema.Type) bool diff --git a/pkg/action/process_test.go b/pkg/action/process_test.go index f775427..db733b0 100644 --- a/pkg/action/process_test.go +++ b/pkg/action/process_test.go @@ -153,7 +153,7 @@ type procTestReplaceOptions = *GenericValueProcessorOptions[struct { N string `yaml:"new"` }] -func addTestValueProcessors(am Manager) { +func addTestValueProcessors(tp TemplateProcessors) { procDefVal := GenericValueProcessor[ValueProcessorOptionsEmpty]{ Fn: func(v any, _ ValueProcessorOptionsEmpty, ctx ValueProcessorContext) (any, error) { if ctx.IsChanged { @@ -182,15 +182,16 @@ func addTestValueProcessors(am Manager) { return v, fmt.Errorf("my_error %q", ctx.DefParam.Name) }, } - am.AddValueProcessor("test.defaultVal", procDefVal) - am.AddValueProcessor("test.replace", procReplace) - am.AddValueProcessor("test.error", procErr) + tp.AddValueProcessor("test.defaultVal", procDefVal) + tp.AddValueProcessor("test.replace", procReplace) + tp.AddValueProcessor("test.error", procErr) } func Test_ActionsValueProcessor(t *testing.T) { t.Parallel() am := NewManager() - addTestValueProcessors(am) + p := NewTemplateProcessors() + addTestValueProcessors(p) tt := []TestCaseValueProcessor{ {"valid processor chain - with defaults, input given", actionProcessWithDefault, nil, nil, @@ -213,7 +214,7 @@ func Test_ActionsValueProcessor(t *testing.T) { tt := tt t.Run(tt.Name, func(t *testing.T) { t.Parallel() - tt.Test(t, am) + tt.Test(t, am, p) }) } } diff --git a/pkg/action/test_utils.go b/pkg/action/test_utils.go index 342f5d7..8730b10 100644 --- a/pkg/action/test_utils.go +++ b/pkg/action/test_utils.go @@ -98,10 +98,10 @@ type TestCaseValueProcessor struct { } // Test runs the test for [ValueProcessor]. -func (tt TestCaseValueProcessor) Test(t *testing.T, am Manager) { +func (tt TestCaseValueProcessor) Test(t *testing.T, am Manager, tp TemplateProcessors) { a := NewFromYAML(tt.Name, []byte(tt.Yaml)) // Init processors in the action. - err := a.SetProcessors(am.GetValueProcessors()) + err := a.setProcessors(tp.GetValueProcessors()) assertIsSameError(t, tt.ErrInit, err) if tt.ErrInit != nil { return diff --git a/pkg/action/yaml.def.go b/pkg/action/yaml.def.go index ab44b93..9dd1c90 100644 --- a/pkg/action/yaml.def.go +++ b/pkg/action/yaml.def.go @@ -16,7 +16,6 @@ const ( sErrFieldMustBeArr = "field must be an array" sErrArrElMustBeObj = "array element must be an object" sErrArrEl = "element must be an array of strings" - sErrArrOrStrEl = "element must be an array of strings or a string" sErrArrOrMapEl = "element must be an array of strings or a key-value object" sErrEmptyRuntimeImg = "image field cannot be empty" @@ -52,9 +51,7 @@ func (err errUnsupportedActionVersion) Is(cmp error) bool { } var ( - rgxUnescTplRow = regexp.MustCompile(`(?:-|\S+:)(?:\s*)?({{.*}}.*)`) - rgxTplRow = regexp.MustCompile(`({{.*}}.*)`) - rgxVarName = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_\\-]*$`) + rgxVarName = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_\\-]*$`) ) // NewDefFromYaml creates an action file definition from yaml configuration. @@ -80,16 +77,6 @@ func NewDefFromYaml(b []byte) (*Definition, error) { return &d, nil } -// NewDefFromYamlTpl creates an action file definition from yaml configuration -// as [NewDefFromYaml] but considers that it has unescaped template values. -func NewDefFromYamlTpl(b []byte) (*Definition, error) { - // Find unescaped occurrences of template elements. - bufRaw := rgxUnescTplRow.ReplaceAllFunc(b, func(match []byte) []byte { - return rgxTplRow.ReplaceAll(match, []byte(`"$1"`)) - }) - return NewDefFromYaml(bufRaw) -} - // Definition is a representation of an action file. type Definition struct { Version string `yaml:"version"` @@ -106,12 +93,7 @@ func (d *Definition) Content() ([]byte, error) { } // Load implements [Loader] interface. -func (d *Definition) Load(_ LoadContext) (*Definition, error) { - return d.LoadRaw() -} - -// LoadRaw implements [Loader] interface. -func (d *Definition) LoadRaw() (*Definition, error) { +func (d *Definition) Load(_ *LoadContext) (*Definition, error) { return d, nil } @@ -354,7 +336,6 @@ type DefParameter struct { // Name is an action unique parameter name used. Name string `yaml:"name"` // Shorthand is a short name 1 syllable name used in Console. - // @todo test definition, validate, catch panic if overlay, add to readme. Shorthand string `yaml:"shorthand"` // Required indicates if the parameter is mandatory. // It's not correct json schema, and it's processed to a correct place later. @@ -404,6 +385,9 @@ func (p *DefParameter) UnmarshalYAML(n *yaml.Node) (err error) { v, err := jsonschema.EnsureType(p.Type, p.Enum[i]) if err != nil { enumNode := yamlFindNodeByKey(n, "enum") + if enumNode == nil { + panic("enum node not found after successfully parsed") + } return yamlTypeErrorLine(err.Error(), enumNode.Line, enumNode.Column) } p.Enum[i] = v diff --git a/pkg/action/yaml.discovery.go b/pkg/action/yaml.discovery.go index dcd0440..6f2fe0c 100644 --- a/pkg/action/yaml.discovery.go +++ b/pkg/action/yaml.discovery.go @@ -1,8 +1,6 @@ package action import ( - "bufio" - "bytes" "io" "path/filepath" "regexp" @@ -36,9 +34,7 @@ func (y YamlDiscoveryStrategy) IsValid(path string) bool { func (y YamlDiscoveryStrategy) Loader(l FileLoadFn, p ...LoadProcessor) Loader { return &YamlFileLoader{ YamlLoader: YamlLoader{ - Processor: NewPipeProcessor( - append([]LoadProcessor{escapeYamlTplCommentsProcessor{}}, p...)..., - ), + Processor: NewPipeProcessor(p...), }, FileOpen: l, } @@ -58,8 +54,7 @@ func (l *YamlLoader) Content() ([]byte, error) { return l.Bytes, nil } -// LoadRaw implements [Loader] interface. -func (l *YamlLoader) LoadRaw() (*Definition, error) { +func (l *YamlLoader) loadRaw() (*Definition, error) { var err error buf, err := l.Content() if err != nil { @@ -68,7 +63,7 @@ func (l *YamlLoader) LoadRaw() (*Definition, error) { l.mx.Lock() defer l.mx.Unlock() if l.raw == nil { - l.raw, err = NewDefFromYamlTpl(buf) + l.raw, err = NewDefFromYaml(buf) if err != nil { return nil, err } @@ -77,24 +72,24 @@ func (l *YamlLoader) LoadRaw() (*Definition, error) { } // Load implements [Loader] interface. -func (l *YamlLoader) Load(ctx LoadContext) (res *Definition, err error) { +func (l *YamlLoader) Load(ctx *LoadContext) (res *Definition, err error) { // Open a file and cache content for future reads. c, err := l.Content() if err != nil { return nil, err } - buf := make([]byte, len(c)) - copy(buf, c) + if ctx == nil { + return l.loadRaw() + } + res, err = NewDefFromYaml(c) if l.Processor != nil { - buf, err = l.Processor.Process(ctx, buf) + err = processStructStringsInPlace(res, func(s string) (string, error) { + return l.Processor.Process(ctx, s) + }) if err != nil { return nil, err } } - res, err = NewDefFromYaml(buf) - if err != nil { - return nil, err - } return res, err } @@ -104,17 +99,8 @@ type YamlFileLoader struct { FileOpen FileLoadFn // FileOpen lazy loads the content of the file. } -// LoadRaw implements [Loader] interface. -func (l *YamlFileLoader) LoadRaw() (*Definition, error) { - _, err := l.Content() - if err != nil { - return nil, err - } - return l.YamlLoader.LoadRaw() -} - // Load implements [Loader] interface. -func (l *YamlFileLoader) Load(ctx LoadContext) (res *Definition, err error) { +func (l *YamlFileLoader) Load(ctx *LoadContext) (res *Definition, err error) { // Open a file and cache content for future reads. _, err = l.Content() if err != nil { @@ -143,27 +129,3 @@ func (l *YamlFileLoader) Content() ([]byte, error) { } return l.Bytes, nil } - -type escapeYamlTplCommentsProcessor struct{} - -func (p escapeYamlTplCommentsProcessor) Process(_ LoadContext, b []byte) ([]byte, error) { - // Read by line. - scanner := bufio.NewScanner(bytes.NewBuffer(b)) - res := make([]byte, 0, len(b)) - for scanner.Scan() { - l := scanner.Bytes() - if i := bytes.IndexByte(l, '#'); i != -1 { - // Check the comment symbol is not inside a string. - // Multiline strings are not supported for now. - if (bytes.LastIndexByte(l[:i], '"') == -1 || bytes.IndexByte(l[i:], '"') == -1) && - (bytes.LastIndexByte(l[:i], '\'') == -1 || bytes.IndexByte(l[i:], '\'') == -1) { - // Strip data after comment symbol. - l = l[:i] - } - } - // Collect the modified lines. - res = append(res, l...) - res = append(res, '\n') - } - return res, nil -} diff --git a/pkg/action/yaml_const_test.go b/pkg/action/yaml_const_test.go index a128841..8afedfb 100644 --- a/pkg/action/yaml_const_test.go +++ b/pkg/action/yaml_const_test.go @@ -82,6 +82,20 @@ runtime: - "${TEST_ENV_1} ${TEST_ENV_UND}" ` +const validMultilineYaml = ` +action: + title: Title +runtime: + type: container + image: python:3.7-slim + env: + MY_MULTILINE_ENV1: "${TEST_MULTILINE_ENV1}" + MY_MULTILINE_ENV2: ${TEST_MULTILINE_ENV1} + MY_MULTILINE_ENV3: | + ${TEST_MULTILINE_ENV1} + command: [pwd] +` + const validCmdArrYaml = ` action: title: Title @@ -414,7 +428,7 @@ runtime: ` // Unescaped template strings. -const validUnescTplStr = ` +const invalidUnescTplStr = ` action: title: Title runtime: @@ -426,24 +440,6 @@ runtime: - {{ .A2 }} {{ .A3 }} asafs ` -const invalidUnescUnsupKeyTplStr = ` -action: - title: Title -runtime: - type: container - image: {{ .A1 }}:latest - {{ .A1 }}: ls -` - -const invalidUnescUnsupArrTplStr = ` -action: - title: Title -runtime: - type: container - image: {{ .A1 }} - command: [{{ .A1 }}, {{ .A1 }}] -` - const validArgString = ` runtime: plugin action: diff --git a/pkg/action/yaml_test.go b/pkg/action/yaml_test.go index 57c3bf1..6ea7f85 100644 --- a/pkg/action/yaml_test.go +++ b/pkg/action/yaml_test.go @@ -78,7 +78,7 @@ func Test_CreateFromYaml(t *testing.T) { {"invalid env declaration - object", invalidEnvObj, yamlTypeErrorLine(sErrArrOrMapEl, 9, 5)}, // Templating. - {"unescaped template val", validUnescTplStr, errTestAny{}}, + {"unescaped template val", invalidUnescTplStr, errTestAny{}}, } for _, tt := range ttYaml { tt := tt @@ -91,27 +91,3 @@ func Test_CreateFromYaml(t *testing.T) { // @todo test that the content is in place } - -func Test_CreateFromYamlTpl(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - input string - expErr error - } - - ttYaml := []testCase{ - {"supported unescaped template val", validUnescTplStr, nil}, - {"unsupported unescaped template key", invalidUnescUnsupKeyTplStr, errTestAny{}}, - {"unsupported unescaped template array", invalidUnescUnsupArrTplStr, errTestAny{}}, - } - for _, tt := range ttYaml { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - _, err := NewDefFromYamlTpl([]byte(tt.input)) - assertIsSameError(t, tt.expErr, err) - }) - } -} diff --git a/plugins/actionnaming/plugin.go b/plugins/actionnaming/plugin.go index ff2b3e8..5598f36 100644 --- a/plugins/actionnaming/plugin.go +++ b/plugins/actionnaming/plugin.go @@ -34,8 +34,8 @@ func (p Plugin) OnAppInit(app launchr.App) error { // Get services. var cfg launchr.Config var am action.Manager - app.GetService(&cfg) - app.GetService(&am) + app.Services().Get(&cfg) + app.Services().Get(&am) // Load naming configuration. var launchrConfig launchrCfg diff --git a/plugins/actionscobra/plugin.go b/plugins/actionscobra/plugin.go index f7105c1..c182c6d 100644 --- a/plugins/actionscobra/plugin.go +++ b/plugins/actionscobra/plugin.go @@ -38,8 +38,8 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { // OnAppInit implements [launchr.Plugin] interface. func (p *Plugin) OnAppInit(app launchr.App) error { p.app = app.(launchr.AppInternal) - app.GetService(&p.am) - app.GetService(&p.pm) + app.Services().Get(&p.am) + app.Services().Get(&p.pm) return p.discoverActions() } diff --git a/plugins/builtinprocessors/plugin.go b/plugins/builtinprocessors/plugin.go index 43ff0b7..b9c0293 100644 --- a/plugins/builtinprocessors/plugin.go +++ b/plugins/builtinprocessors/plugin.go @@ -28,8 +28,8 @@ func (p Plugin) OnAppInit(app launchr.App) error { // Get services. var cfg launchr.Config var am action.Manager - app.GetService(&cfg) - app.GetService(&am) + app.Services().Get(&cfg) + app.Services().Get(&am) addValueProcessors(am, cfg) @@ -44,13 +44,15 @@ type ConfigGetProcessorOptions = *action.GenericValueProcessorOptions[struct { }] // addValueProcessors submits new [action.ValueProcessor] to [action.Manager]. -func addValueProcessors(m action.Manager, cfg launchr.Config) { +func addValueProcessors(tp action.TemplateProcessors, cfg launchr.Config) { procCfg := action.GenericValueProcessor[ConfigGetProcessorOptions]{ Fn: func(v any, opts ConfigGetProcessorOptions, ctx action.ValueProcessorContext) (any, error) { return processorConfigGetByKey(v, opts, ctx, cfg) }, } - m.AddValueProcessor(procGetConfigValue, procCfg) + tp.AddValueProcessor(procGetConfigValue, procCfg) + tplCfg := &configTemplateFunc{cfg: cfg} + tp.AddTemplateFunc("config", func() *configTemplateFunc { return tplCfg }) } func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.ValueProcessorContext, cfg launchr.Config) (any, error) { @@ -68,3 +70,39 @@ func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.V return jsonschema.EnsureType(ctx.DefParam.Type, res) } + +// configKeyNotFound holds a config key element that was not found in config. +// It will print a message in a template when a config key is missing. +type configKeyNotFound string + +// IsEmpty implements a special interface to support "default" template function +// Example: {{ config.Get "foo.bar" | default "buz" }} +func (s configKeyNotFound) IsEmpty() bool { return true } + +// String implements [fmt.Stringer] to output a missing key to a template. +func (s configKeyNotFound) String() string { return "" } + +// configTemplateFunc is a set of template functions to interact with [launchr.Config] in [action.TemplateProcessors]. +type configTemplateFunc struct { + cfg launchr.Config +} + +// Get returns a config value by a path. +// +// Usage: +// +// {{ config.Get "foo.bar" }} - retrieves value of any type +// {{ index (config.Get "foo.array-elem") 1 }} - retrieves specific array element +// {{ config.Get "foo.null-elem" | default "foo" }} - uses default if value is nil +// {{ config.Get "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist +func (t *configTemplateFunc) Get(path string) (any, error) { + var res any + if !t.cfg.Exists(path) { + return configKeyNotFound(path), nil + } + err := t.cfg.Get(path, &res) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/plugins/builtinprocessors/plugin_test.go b/plugins/builtinprocessors/plugin_test.go index df0dc0f..38b5e79 100644 --- a/plugins/builtinprocessors/plugin_test.go +++ b/plugins/builtinprocessors/plugin_test.go @@ -4,6 +4,9 @@ import ( "testing" "testing/fstest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/action" "github.com/launchrctl/launchr/pkg/jsonschema" @@ -62,12 +65,49 @@ action: - processor: config.GetValue ` +const testTplConfigGet = ` +action: + title: test config +runtime: + type: container + image: alpine + command: + - '{{ config.Get "my.string" }}' + - '{{ config.Get "my.int" }}' + - '{{ config.Get "my.bool" }}' + - '{{ config.Get "my.array" }}' + - '{{ index (config.Get "my.array") 1 }}' + - '{{ config.Get "my.null" | default "foo" }}' + - '{{ config.Get "my.missing" | default "bar" }}' +` + +const testTplConfigGetMissing = ` +action: + title: test config +runtime: + type: container + image: alpine + command: + - '{{ config.Get "my.missing" }}' +` + +const testTplConfigGetWrongCall = ` +action: + title: test config +runtime: + type: container + image: alpine + command: + - '{{ config.Get }}' +` + const testConfig = ` my: string: my_str int: 42 bool: true array: ["1", "2", "3"] + null: null ` func testConfigFS(s string) launchr.Config { @@ -81,7 +121,8 @@ func Test_ConfigProcessor(t *testing.T) { // Prepare services. cfg := testConfigFS(testConfig) am := action.NewManager() - addValueProcessors(am, cfg) + tp := action.NewTemplateProcessors() + addValueProcessors(tp, cfg) expConfig := action.InputParams{ "string": "my_str", @@ -105,7 +146,45 @@ func Test_ConfigProcessor(t *testing.T) { tt := tt t.Run(tt.Name, func(t *testing.T) { t.Parallel() - tt.Test(t, am) + tt.Test(t, am, tp) + }) + } +} + +func Test_ConfigTemplateFunc(t *testing.T) { + // Prepare services. + cfg := testConfigFS(testConfig) + tp := action.NewTemplateProcessors() + addValueProcessors(tp, cfg) + svc := launchr.NewServiceManager() + svc.Add(tp) + + type testCase struct { + Name string + Yaml string + Exp []string + Err string + } + + tt := []testCase{ + {Name: "valid", Yaml: testTplConfigGet, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}}, + {Name: "key not found", Yaml: testTplConfigGetMissing, Exp: []string{""}}, + {Name: "incorrect call", Yaml: testTplConfigGetWrongCall, Err: "wrong number of args for Get: want 1 got 0"}, + } + for _, tt := range tt { + tt := tt + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + a := action.NewFromYAML(tt.Name, []byte(tt.Yaml)) + a.SetServices(svc) + err := a.EnsureLoaded() + if tt.Err != "" { + require.ErrorContains(t, err, tt.Err) + return + } + require.NoError(t, err) + rdef := a.RuntimeDef() + assert.Equal(t, tt.Exp, []string(rdef.Container.Command)) }) } } diff --git a/plugins/verbosity/plugin.go b/plugins/verbosity/plugin.go index afc8786..1862975 100644 --- a/plugins/verbosity/plugin.go +++ b/plugins/verbosity/plugin.go @@ -171,7 +171,7 @@ func (p Plugin) OnAppInit(app launchr.App) error { cmd.SetErr(streams.Err()) var am action.Manager - app.GetService(&am) + app.Services().Get(&am) // Retrieve and expand application persistent flags with new log and term-related options. persistentFlags := am.GetPersistentFlags() diff --git a/plugins/yamldiscovery/plugin.go b/plugins/yamldiscovery/plugin.go index 5a6babf..d8eb252 100644 --- a/plugins/yamldiscovery/plugin.go +++ b/plugins/yamldiscovery/plugin.go @@ -33,7 +33,7 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { // OnAppInit implements [launchr.Plugin] interface to provide discovered actions. func (p *Plugin) OnAppInit(app launchr.App) error { - app.GetService(&p.am) + app.Services().Get(&p.am) p.app = app return nil } diff --git a/test/plugins/testactions/plugin.go b/test/plugins/testactions/plugin.go index 7b802cb..8c99ca3 100644 --- a/test/plugins/testactions/plugin.go +++ b/test/plugins/testactions/plugin.go @@ -26,7 +26,7 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { func (p *Plugin) OnAppInit(app launchr.App) error { p.app = app var am action.Manager - app.GetService(&am) + app.Services().Get(&am) // Add custom fs to default discovery. app.RegisterFS(action.NewDiscoveryFS(registeredEmbedFS, app.GetWD())) // Create a special decorator to output given input. diff --git a/test/testdata/runtime/container/image-build.txtar b/test/testdata/runtime/container/image-build.txtar index ee0e32d..9e4b7c6 100644 --- a/test/testdata/runtime/container/image-build.txtar +++ b/test/testdata/runtime/container/image-build.txtar @@ -232,8 +232,8 @@ runtime: context: ./context # Custom build context subdirectory buildfile: test.Dockerfile # Custom Dockerfile name args: - USER_ID: {{ .current_uid }} # Template: current user ID - GROUP_ID: {{ .current_gid }} # Template: current group ID + USER_ID: "$UID" # Template: current user ID + GROUP_ID: "$UID" # Template: current group ID USER_NAME: foobar # Static build argument command: - id # Command to display user info diff --git a/types.go b/types.go index c9d183c..958fd6a 100644 --- a/types.go +++ b/types.go @@ -99,6 +99,8 @@ type ( GenerateConfig = launchr.GenerateConfig // PluginManager handles plugins. PluginManager = launchr.PluginManager + // ServiceManager is a basic Dependency Injection container storing registered [Service]. + ServiceManager = launchr.ServiceManager // ServiceInfo provides service info for its initialization. ServiceInfo = launchr.ServiceInfo // Service is a common interface for a service to register. @@ -157,6 +159,9 @@ func NoopStreams() Streams { return launchr.NoopStreams() } // the standard [os.Stdin], [os.Stdout] and [os.Stderr]. func StdInOutErr() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { return launchr.StdInOutErr() } +// NewServiceManager initializes ServiceManager. +func NewServiceManager() ServiceManager { return launchr.NewServiceManager() } + // NewMaskingWriter initializes a new MaskingWriter. func NewMaskingWriter(w io.Writer, mask *SensitiveMask) io.WriteCloser { return launchr.NewMaskingWriter(w, mask) From e8222105253ec3f70886ae8696ef263b5908802b Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Mon, 22 Sep 2025 22:17:44 +0200 Subject: [PATCH 04/15] Update dependencies. --- go.mod | 22 +++++++++--------- go.sum | 29 ++++++++++++++++++++++++ plugins/builtinprocessors/plugin_test.go | 6 ++--- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 56823e6..f88e9b0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.1 require ( github.com/containerd/errdefs v1.0.0 - github.com/docker/docker v28.3.2+incompatible + github.com/docker/docker v28.4.0+incompatible github.com/knadh/koanf v1.5.0 github.com/moby/go-archive v0.1.0 github.com/moby/sys/signal v0.7.1 @@ -14,13 +14,13 @@ require ( github.com/pterm/pterm v0.12.81 github.com/rogpeppe/go-internal v1.14.1 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.7 - github.com/stretchr/testify v1.10.0 - go.uber.org/mock v0.5.2 - golang.org/x/mod v0.26.0 - golang.org/x/sys v0.34.0 - golang.org/x/text v0.27.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + go.uber.org/mock v0.6.0 + golang.org/x/mod v0.28.0 + golang.org/x/sys v0.36.0 + golang.org/x/text v0.29.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.33.1 k8s.io/apimachinery v0.33.1 @@ -93,11 +93,11 @@ require ( go.opentelemetry.io/otel/trace v1.36.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/term v0.33.0 // indirect + golang.org/x/term v0.34.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/protobuf v1.36.6 // indirect diff --git a/go.sum b/go.sum index 4f67e3e..1ff5338 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= +github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -394,10 +396,15 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -410,6 +417,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -445,6 +454,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= @@ -472,6 +483,10 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -493,6 +508,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -550,6 +567,10 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -557,6 +578,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -568,6 +591,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= @@ -586,6 +613,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/plugins/builtinprocessors/plugin_test.go b/plugins/builtinprocessors/plugin_test.go index 38b5e79..0103cba 100644 --- a/plugins/builtinprocessors/plugin_test.go +++ b/plugins/builtinprocessors/plugin_test.go @@ -91,14 +91,14 @@ runtime: - '{{ config.Get "my.missing" }}' ` -const testTplConfigGetWrongCall = ` +const testTplConfigGetBadArgs = ` action: title: test config runtime: type: container image: alpine command: - - '{{ config.Get }}' + - '{{ config.Get "my.string" "my.string" }}' ` const testConfig = ` @@ -169,7 +169,7 @@ func Test_ConfigTemplateFunc(t *testing.T) { tt := []testCase{ {Name: "valid", Yaml: testTplConfigGet, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}}, {Name: "key not found", Yaml: testTplConfigGetMissing, Exp: []string{""}}, - {Name: "incorrect call", Yaml: testTplConfigGetWrongCall, Err: "wrong number of args for Get: want 1 got 0"}, + {Name: "incorrect call", Yaml: testTplConfigGetBadArgs, Err: "wrong number of args for Get: want 1 got 2"}, } for _, tt := range tt { tt := tt From 5cef0770eec821abd045f938e5dc9e66049907bf Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Mon, 22 Sep 2025 22:20:14 +0200 Subject: [PATCH 05/15] Update dependencies. --- go.mod | 73 ++++++++++++---------- go.sum | 187 +++++++++++++++++++++++++++------------------------------ 2 files changed, 132 insertions(+), 128 deletions(-) diff --git a/go.mod b/go.mod index f88e9b0..1917a9f 100644 --- a/go.mod +++ b/go.mod @@ -22,9 +22,9 @@ require ( golang.org/x/sys v0.36.0 golang.org/x/text v0.29.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.1 - k8s.io/apimachinery v0.33.1 - k8s.io/client-go v0.33.1 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 ) require ( @@ -38,28 +38,38 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gookit/color v1.5.4 // indirect + github.com/gookit/color v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -72,7 +82,7 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect @@ -85,29 +95,30 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/time v0.13.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/yaml v1.5.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 1ff5338..3d1d712 100644 --- a/go.sum +++ b/go.sum @@ -80,17 +80,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= -github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -103,8 +101,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -118,12 +116,34 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -150,8 +170,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -161,7 +181,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -170,10 +189,12 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAx github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -257,8 +278,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -314,8 +335,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -394,14 +416,9 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -415,8 +432,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -431,37 +446,35 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= @@ -481,10 +494,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -506,15 +515,13 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -565,10 +572,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -576,10 +579,8 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -589,15 +590,11 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -611,8 +608,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -652,16 +647,16 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -681,26 +676,24 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= -sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 9f940b52d08d0f9ff3c41b938ccd1b3c7041c142 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sat, 27 Sep 2025 20:38:44 +0200 Subject: [PATCH 06/15] Fix linter. --- pkg/action/action.flags.go | 4 ++-- pkg/action/discover.go | 6 +++--- pkg/action/manager.go | 2 +- pkg/action/runtime.container.go | 6 +++--- plugins/actionscobra/cobra.go | 4 ++-- plugins/verbosity/plugin.go | 12 ++++++------ 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/action/action.flags.go b/pkg/action/action.flags.go index ab71b49..cc0fe64 100644 --- a/pkg/action/action.flags.go +++ b/pkg/action/action.flags.go @@ -25,8 +25,8 @@ func NewFlagsGroup(name string) *FlagsGroup { } } -// GetName returns the name of the flags group. -func (p *FlagsGroup) GetName() string { +// Name returns the name of the flags group. +func (p *FlagsGroup) Name() string { return p.name } diff --git a/pkg/action/discover.go b/pkg/action/discover.go index c0951f6..0a9277d 100644 --- a/pkg/action/discover.go +++ b/pkg/action/discover.go @@ -64,8 +64,8 @@ func (f DiscoveryFS) Realpath() string { return f.real } -// IsExists checks if the underlying filesystem path is available for discovery. -func (f DiscoveryFS) IsExists() bool { +// IsDiscoverable checks if the underlying filesystem path is available for discovery. +func (f DiscoveryFS) IsDiscoverable() bool { if f.real == "" { // Virtual fs is always available. return true @@ -173,7 +173,7 @@ func (ad *Discovery) Discover(ctx context.Context) ([]*Action, error) { defer launchr.EstimateTime(func(diff time.Duration) { launchr.Log().Debug("action discovering elapsed time", "time", diff.Round(time.Millisecond), "path", ad.fs.Realpath()) }) - if !ad.fs.IsExists() { + if !ad.fs.IsDiscoverable() { return nil, nil } wg := sync.WaitGroup{} diff --git a/pkg/action/manager.go b/pkg/action/manager.go index cca66b4..93d9ffe 100644 --- a/pkg/action/manager.go +++ b/pkg/action/manager.go @@ -369,7 +369,7 @@ func (m *actionManagerMap) ValidateInput(a *Action, input *Input) error { } persistentFlags := m.GetPersistentFlags() - err := persistentFlags.ValidateFlags(input.GroupFlags(persistentFlags.GetName())) + err := persistentFlags.ValidateFlags(input.GroupFlags(persistentFlags.Name())) if err != nil { return err } diff --git a/pkg/action/runtime.container.go b/pkg/action/runtime.container.go index 8e197d1..9477522 100644 --- a/pkg/action/runtime.container.go +++ b/pkg/action/runtime.container.go @@ -165,13 +165,13 @@ func (c *runtimeContainer) GetFlags() *FlagsGroup { } func (c *runtimeContainer) ValidateInput(input *Input) error { - err := c.flags.ValidateFlags(input.GroupFlags(c.flags.GetName())) + err := c.flags.ValidateFlags(input.GroupFlags(c.flags.Name())) if err != nil { return err } // early peak for an exec flag. - exec := input.GetFlagInGroup(c.flags.GetName(), containerFlagExec) + exec := input.GetFlagInGroup(c.flags.Name(), containerFlagExec) if exec != nil && exec.(bool) { // Mark input as validated because arguments are passed directly to exec. input.SetValidated(true) @@ -181,7 +181,7 @@ func (c *runtimeContainer) ValidateInput(input *Input) error { } func (c *runtimeContainer) SetFlags(input *Input) error { - flags := input.GroupFlags(c.flags.GetName()) + flags := input.GroupFlags(c.flags.Name()) if v, ok := flags[containerFlagRemote]; ok { c.isSetRemote = v.(bool) diff --git a/plugins/actionscobra/cobra.go b/plugins/actionscobra/cobra.go index a9856f7..b766cdf 100644 --- a/plugins/actionscobra/cobra.go +++ b/plugins/actionscobra/cobra.go @@ -40,7 +40,7 @@ func CobraImpl(a *action.Action, streams launchr.Streams, manager action.Manager if runOpts[flag] != nil { value = runOpts[flag] } - input.SetFlagInGroup(runtimeFlagsGroup.GetName(), flag, value) + input.SetFlagInGroup(runtimeFlagsGroup.Name(), flag, value) } } @@ -49,7 +49,7 @@ func CobraImpl(a *action.Action, streams launchr.Streams, manager action.Manager // Flags are immutable in action. persistentFlagsGroup := manager.GetPersistentFlags() for k, v := range persistentFlagsGroup.GetAll() { - input.SetFlagInGroup(persistentFlagsGroup.GetName(), k, v) + input.SetFlagInGroup(persistentFlagsGroup.Name(), k, v) } // Validate input before setting to action. diff --git a/plugins/verbosity/plugin.go b/plugins/verbosity/plugin.go index 1862975..ca01593 100644 --- a/plugins/verbosity/plugin.go +++ b/plugins/verbosity/plugin.go @@ -103,8 +103,8 @@ func (p Plugin) OnAppInit(app launchr.App) error { cmd := appInternal.RootCmd() pflags := cmd.PersistentFlags() // Make sure not to fail on unknown flags because we are parsing early. - unkFlagsBkp := pflags.ParseErrorsWhitelist.UnknownFlags - pflags.ParseErrorsWhitelist.UnknownFlags = true + unkFlagsBkp := pflags.ParseErrorsAllowlist.UnknownFlags + pflags.ParseErrorsAllowlist.UnknownFlags = true pflags.CountVarP(&verbosity, "verbose", "v", "log verbosity level, use -vvvv DEBUG, -vvv INFO, -vv WARN, -v ERROR") pflags.VarP(&logLvlStr, "log-level", "", "log level, same as -v, can be: DEBUG, INFO, WARN, ERROR or NONE (default NONE)") pflags.VarP(&logFormatStr, "log-format", "", "log format, can be: pretty, plain or json (default pretty)") @@ -119,7 +119,7 @@ func (p Plugin) OnAppInit(app launchr.App) error { // It shouldn't happen here. panic(err) } - pflags.ParseErrorsWhitelist.UnknownFlags = unkFlagsBkp + pflags.ParseErrorsAllowlist.UnknownFlags = unkFlagsBkp // Set quiet mode. launchr.Term().EnableOutput() @@ -239,12 +239,12 @@ func withCustomLogger(m action.Manager, a *action.Action) { persistentFlags := m.GetPersistentFlags() if rt, ok := a.Runtime().(action.RuntimeLoggerAware); ok { var logFormat LogFormat - if lfStr, ok := a.Input().GetFlagInGroup(persistentFlags.GetName(), "log-format").(string); ok { + if lfStr, ok := a.Input().GetFlagInGroup(persistentFlags.Name(), "log-format").(string); ok { logFormat = LogFormat(lfStr) } var logLevel launchr.LogLevel - if llStr, ok := a.Input().GetFlagInGroup(persistentFlags.GetName(), "log-level").(string); ok { + if llStr, ok := a.Input().GetFlagInGroup(persistentFlags.Name(), "log-level").(string); ok { logLevel = launchr.LogLevelFromString(llStr) } @@ -267,7 +267,7 @@ func withCustomTerm(m action.Manager, a *action.Action) { if rt, ok := a.Runtime().(action.RuntimeTermAware); ok { term := launchr.NewTerminal() term.SetOutput(a.Input().Streams().Out()) - if quiet, ok := a.Input().GetFlagInGroup(persistentFlags.GetName(), "log-level").(bool); ok && quiet { + if quiet, ok := a.Input().GetFlagInGroup(persistentFlags.Name(), "log-level").(bool); ok && quiet { term.DisableOutput() } From df6538f3d27bf5775f270882871c68faee5cb224 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sat, 27 Sep 2025 20:50:16 +0200 Subject: [PATCH 07/15] Fix linter. --- gen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gen.go b/gen.go index a836804..945585e 100644 --- a/gen.go +++ b/gen.go @@ -46,7 +46,7 @@ func (app *appImpl) gen() error { // Do not fail if some flags change in future builds. flags := app.cmd.Flags() - flags.ParseErrorsWhitelist.UnknownFlags = true + flags.ParseErrorsAllowlist.UnknownFlags = true // Working directory flag is helpful because "go run" can't be run outside // a project directory and use all its go.mod dependencies. flags.StringVar(&config.WorkDir, "work-dir", config.WorkDir, "Working directory") From 6f2a4e8fdf349906e0333a8d231d49a9a270ed3a Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sat, 27 Sep 2025 20:52:44 +0200 Subject: [PATCH 08/15] Fix linter. --- .github/workflows/test-suite.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index e735bdf..9b2fa17 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -68,6 +68,7 @@ jobs: os: ubuntu-24.04-arm - name: 🍎 MacOS (amd64) os: macos-13 + continue-on-error: true - name: 🍎 MacOS (arm64) os: macos-latest needs-sidecar: true @@ -76,6 +77,7 @@ jobs: - name: 🖥️ Windows (arm64) os: windows-11-arm needs-sidecar: true + continue-on-error: true runs-on: ${{ matrix.os }} needs: [ client-ssh-key ] continue-on-error: ${{ matrix.continue-on-error || false }} From 1d8d78f77b1eb20e1e4290cf6ca7f3be96186591 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sat, 27 Sep 2025 21:46:49 +0200 Subject: [PATCH 09/15] Update linter. --- .github/workflows/test-suite.yaml | 1 - Makefile | 2 +- docs/development/test.md | 2 +- go.mod | 4 +--- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 9b2fa17..13ae2d9 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -141,7 +141,6 @@ jobs: name: test-log-${{ matrix.os }} path: .gotmp/gotest.log retention-days: 3 - if-no-files-found: error lint: name: 🧹 Lint & Code Quality diff --git a/Makefile b/Makefile index 44656ab..22c4e8d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ LOCAL_BIN:=$(CURDIR)/bin # Linter config. GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint -GOLANGCI_TAG:=2.3.0 +GOLANGCI_TAG:=2.5.0 GOTESTFMT_BIN:=$(GOBIN)/gotestfmt diff --git a/docs/development/test.md b/docs/development/test.md index 2923992..861f608 100644 --- a/docs/development/test.md +++ b/docs/development/test.md @@ -198,7 +198,7 @@ concurrency: jobs: tests: name: 🛡️ Multi-Platform Testing Suite - uses: launchr/launchr/.github/workflows/test-suite.yaml@main + uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main ``` ### Benefits of Reusable Workflows diff --git a/go.mod b/go.mod index 1917a9f..d7462b9 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/launchrctl/launchr -go 1.24.0 - -toolchain go1.24.1 +go 1.25.0 require ( github.com/containerd/errdefs v1.0.0 From 4543b7a296044327408428122614c57c51339b40 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sun, 28 Sep 2025 21:57:06 +0200 Subject: [PATCH 10/15] Refactor services and the way they are created. --- app.go | 48 ++++------ internal/launchr/config.go | 26 +++--- internal/launchr/sensitive.go | 72 +++++++++++---- internal/launchr/sensitive_test.go | 7 +- internal/launchr/services.go | 72 +++++++++++---- internal/launchr/streams.go | 4 +- internal/launchr/types.go | 11 +-- pkg/action/action.go | 6 +- pkg/action/action.input.go | 5 + pkg/action/loader.go | 38 +++++--- pkg/action/loader_test.go | 69 +++++++++++--- pkg/action/manager.go | 127 +++++++++++--------------- pkg/action/process.go | 69 +++++++++----- pkg/action/process_test.go | 8 +- pkg/action/test_utils.go | 2 +- plugins/builtinprocessors/plugin.go | 8 +- plugins/yamldiscovery/plugin.go | 4 + test/plugins/genaction/go.mod | 2 +- test/plugins/testactions/plugin.go | 4 +- test/plugins/testactions/sensitive.go | 5 +- types.go | 2 +- 21 files changed, 350 insertions(+), 239 deletions(-) diff --git a/app.go b/app.go index f565eb8..ea86983 100644 --- a/app.go +++ b/app.go @@ -6,7 +6,6 @@ import ( "os" "github.com/launchrctl/launchr/internal/launchr" - "github.com/launchrctl/launchr/pkg/action" _ "github.com/launchrctl/launchr/plugins" // include default plugins ) @@ -18,11 +17,11 @@ type appImpl struct { // FS related. mFS []ManagedFS workDir string - cfgDir string // Services. streams Streams - services ServiceManager + mask *SensitiveMask + services *ServiceManager pluginMngr PluginManager } @@ -38,15 +37,13 @@ func (app *appImpl) SetStreams(s Streams) { app.streams = s } func (app *appImpl) RegisterFS(fs ManagedFS) { app.mFS = append(app.mFS, fs) } func (app *appImpl) GetRegisteredFS() []ManagedFS { return app.mFS } -func (app *appImpl) SensitiveWriter(w io.Writer) io.Writer { - return NewMaskingWriter(w, app.SensitiveMask()) -} -func (app *appImpl) SensitiveMask() *SensitiveMask { return launchr.GlobalSensitiveMask() } +func (app *appImpl) SensitiveWriter(w io.Writer) io.Writer { return app.SensitiveMask().MaskWriter(w) } +func (app *appImpl) SensitiveMask() *SensitiveMask { return app.mask } func (app *appImpl) RootCmd() *Command { return app.cmd } func (app *appImpl) CmdEarlyParsed() launchr.CmdEarlyParsed { return app.earlyCmd } -func (app *appImpl) Services() ServiceManager { return app.services } +func (app *appImpl) Services() *ServiceManager { return app.services } func (app *appImpl) AddService(s Service) { app.services.Add(s) @@ -82,38 +79,25 @@ func (app *appImpl) init() error { } app.cmd.SetVersionTemplate(`{{ appVersionFull }}`) app.earlyCmd = launchr.EarlyPeekCommand() - // Set io streams. - app.SetStreams(MaskedStdStreams(app.SensitiveMask())) - app.cmd.SetIn(app.streams.In().Reader()) - app.cmd.SetOut(app.streams.Out()) - app.cmd.SetErr(app.streams.Err()) + app.mask = launchr.NewSensitiveMask("****") - // Set working dir and config dir. - app.cfgDir = "." + name - app.workDir = launchr.MustAbs(".") - actionsPath := launchr.MustAbs(EnvVarActionsPath.Get()) - // Initialize managed FS for action discovery. + // Initialize managed FS for action discovery and set working dir + app.workDir = MustAbs(".") app.mFS = make([]ManagedFS, 0, 4) - app.RegisterFS(action.NewDiscoveryFS(os.DirFS(actionsPath), app.GetWD())) // Prepare dependencies. app.services = launchr.NewServiceManager() app.pluginMngr = launchr.NewPluginManagerWithRegistered() - // @todo consider home dir for global config. - config := launchr.ConfigFromFS(os.DirFS(app.cfgDir)) - actionProcs := action.NewTemplateProcessors() - actionMngr := action.NewManager( - action.WithDefaultRuntime(config), - action.WithContainerRuntimeConfig(config, name+"_"), - action.WithServices(app.services), - ) - actionMngr.SetTemplateProcessors(actionProcs) // Register svcMngr for other modules. - app.services.Add(actionProcs) - app.services.Add(actionMngr) + app.services.Add(app.mask) app.services.Add(app.pluginMngr) - app.services.Add(config) + + // Set io streams. + app.SetStreams(MaskedStdStreams(app.mask)) + app.cmd.SetIn(app.streams.In().Reader()) + app.cmd.SetOut(app.streams.Out()) + app.cmd.SetErr(app.streams.Err()) Log().Debug("initialising application") @@ -126,7 +110,7 @@ func (app *appImpl) init() error { return err } } - Log().Debug("init success", "wd", app.workDir, "actions_dir", actionsPath) + Log().Debug("init success", "wd", app.workDir) return nil } diff --git a/internal/launchr/config.go b/internal/launchr/config.go index 1997636..1456bf5 100644 --- a/internal/launchr/config.go +++ b/internal/launchr/config.go @@ -3,6 +3,7 @@ package launchr import ( "errors" "io/fs" + "os" "path/filepath" "reflect" "regexp" @@ -21,19 +22,7 @@ var ( ) // Config is a launchr config storage interface. -type Config interface { - Service - // DirPath returns an absolute path to config directory. - DirPath() string - // Path provides an absolute path to launchr config directory. - Path(parts ...string) string - // Exists checks if key exists in config. Key level delimiter is dot. - // For example - `path.to.something`. - Exists(key string) bool - // Get returns a value by key to a parameter v. Parameter v must be a pointer to a value. - // Error may be returned on decode. - Get(key string, v any) error -} +type Config = *config type cachedProps = map[string]reflect.Value type config struct { @@ -72,6 +61,12 @@ func (cfg *config) ServiceInfo() ServiceInfo { return ServiceInfo{} } +func (cfg *config) ServiceCreate(_ *ServiceManager) Service { + cfgDir := "." + name + return ConfigFromFS(os.DirFS(cfgDir)) +} + +// DirPath returns an absolute path to config directory. func (cfg *config) DirPath() string { return cfg.rootPath } @@ -80,6 +75,8 @@ func (cfg *config) exists(path string) bool { return cfg.koanf != nil && cfg.koanf.Exists(path) } +// Exists checks if key exists in config. Key level delimiter is dot. +// For example - `path.to.something`. func (cfg *config) Exists(path string) bool { var v any err := cfg.Get(path, &v) @@ -89,6 +86,8 @@ func (cfg *config) Exists(path string) bool { return cfg.exists(path) } +// Get returns a value by key to a parameter v. Parameter v must be a pointer to a value. +// Error may be returned on decode. func (cfg *config) Get(key string, v any) error { cfg.mx.Lock() defer cfg.mx.Unlock() @@ -146,6 +145,7 @@ func (cfg *config) parse() error { return nil } +// Path provides an absolute path to launchr config directory. func (cfg *config) Path(parts ...string) string { parts = append([]string{cfg.rootPath}, parts...) return filepath.Clean(filepath.Join(parts...)) diff --git a/internal/launchr/sensitive.go b/internal/launchr/sensitive.go index ea44aed..2bcf074 100644 --- a/internal/launchr/sensitive.go +++ b/internal/launchr/sensitive.go @@ -3,19 +3,9 @@ package launchr import ( "bytes" "io" + "sync" ) -var globalSensitiveMask *SensitiveMask - -func init() { - globalSensitiveMask = NewSensitiveMask("****") -} - -// GlobalSensitiveMask returns global app sensitive mask. -func GlobalSensitiveMask() *SensitiveMask { - return globalSensitiveMask -} - // MaskingWriter is a writer that masks sensitive data in the input stream. // It buffers data to handle cases where sensitive data spans across writes. type MaskingWriter struct { @@ -95,7 +85,7 @@ func (m *MaskingWriter) shouldFlush(p []byte) bool { // hasPotentialSensitiveData checks if buffer might contain partial sensitive data func (m *MaskingWriter) hasPotentialSensitiveData() bool { - if len(m.mask.strings) == 0 { + if m.mask == nil || len(m.mask.strings) == 0 { return false } @@ -143,10 +133,55 @@ func (m *MaskingWriter) Close() error { // SensitiveMask replaces sensitive strings with a mask. type SensitiveMask struct { + mx sync.Mutex strings [][]byte mask []byte } +// ServiceInfo implements [Service] interface. +func (p *SensitiveMask) ServiceInfo() ServiceInfo { + return ServiceInfo{} +} + +// ServiceCreate implements [ServiceCreate] interface. +func (p *SensitiveMask) ServiceCreate(_ *ServiceManager) Service { + return NewSensitiveMask("****") +} + +// MaskWriter returns a wrapped writer with masked output. +func (p *SensitiveMask) MaskWriter(w io.Writer) io.WriteCloser { + return NewMaskingWriter(w, p) +} + +// Clone creates a copy of a sensitive mask. +func (p *SensitiveMask) Clone() *SensitiveMask { + p.mx.Lock() + defer p.mx.Unlock() + + // Create a new slice with the same length and capacity + clonedStrings := make([][]byte, len(p.strings)) + + // Deep copy each []byte slice + for i, str := range p.strings { + if str != nil { + clonedStrings[i] = make([]byte, len(str)) + copy(clonedStrings[i], str) + } + } + + // Clone the mask slice as well + var clonedMask []byte + if p.mask != nil { + clonedMask = make([]byte, len(p.mask)) + copy(clonedMask, p.mask) + } + + return &SensitiveMask{ + strings: clonedStrings, + mask: clonedMask, + } +} + // String implements [fmt.Stringer] to occasionally not render sensitive data. func (p *SensitiveMask) String() string { return "" } @@ -162,7 +197,7 @@ func (p *SensitiveMask) ReplaceAll(b []byte) (resultBytes []byte, lastBefore, la lastBefore = -1 lastAfter = -1 - if len(p.strings) == 0 { + if p == nil || len(p.strings) == 0 { return b, lastBefore, lastAfter } @@ -210,17 +245,14 @@ func (p *SensitiveMask) ReplaceAll(b []byte) (resultBytes []byte, lastBefore, la // AddString adds a string to mask. func (p *SensitiveMask) AddString(s string) { + p.mx.Lock() + defer p.mx.Unlock() p.strings = append(p.strings, []byte(s)) } // NewSensitiveMask creates a sensitive mask replacing strings with mask value. -func NewSensitiveMask(mask string, strings ...string) *SensitiveMask { - bytestrings := make([][]byte, len(strings)) - for i := 0; i < len(strings); i++ { - bytestrings[i] = []byte(strings[i]) - } +func NewSensitiveMask(mask string) *SensitiveMask { return &SensitiveMask{ - mask: []byte(mask), - strings: bytestrings, + mask: []byte(mask), } } diff --git a/internal/launchr/sensitive_test.go b/internal/launchr/sensitive_test.go index 1bbc06b..6b9c5ed 100644 --- a/internal/launchr/sensitive_test.go +++ b/internal/launchr/sensitive_test.go @@ -16,7 +16,10 @@ func Test_MaskingWriter(t *testing.T) { mask *SensitiveMask // Mask replacement exp string // Expected output after masking } - mask := NewSensitiveMask("****", "987-65-4321", "123-45-6789", "\"\\escaped\nnewline") + mask := NewSensitiveMask("****") + mask.AddString("987-65-4321") + mask.AddString("123-45-6789") + mask.AddString("\"\\escaped\nnewline") tests := []testCase{ { name: "Empty mask", @@ -57,7 +60,7 @@ func Test_MaskingWriter(t *testing.T) { out := &bytes.Buffer{} // Create the MaskingWriter wrapping the out - mwriter := NewMaskingWriter(out, tt.mask) + mwriter := tt.mask.MaskWriter(out) // Simulate multiple writes for _, part := range tt.chunks { diff --git a/internal/launchr/services.go b/internal/launchr/services.go index 1c2d87b..70c223a 100644 --- a/internal/launchr/services.go +++ b/internal/launchr/services.go @@ -20,42 +20,43 @@ type Service interface { ServiceInfo() ServiceInfo } +// ServiceCreate allows creating a service using a ServiceManager. +// TODO: Merge with Service when refactored. +type ServiceCreate interface { + Service + ServiceCreate(svc *ServiceManager) Service +} + // InitServiceInfo sets private fields for internal usage only. func InitServiceInfo(si *ServiceInfo, s Service) { si.pkgPath, si.typeName = GetTypePkgPathName(s) } // ServiceManager is a basic Dependency Injection container storing registered [Service]. -type ServiceManager interface { - // Add registers a service. - // Panics if a service is not unique. - Add(s Service) - // Get retrieves a service of type [v] and assigns it to [v]. - // Panics if a service is not found. - Get(v any) -} - -type serviceManager struct { +type ServiceManager struct { services map[ServiceInfo]Service } // NewServiceManager initializes ServiceManager. -func NewServiceManager() ServiceManager { - return &serviceManager{ +func NewServiceManager() *ServiceManager { + return &ServiceManager{ services: make(map[ServiceInfo]Service), } } -func (sm *serviceManager) Add(s Service) { - info := s.ServiceInfo() - InitServiceInfo(&info, s) +// Add registers a service. +// Panics if a service is not unique. +func (sm *ServiceManager) Add(s Service) { + info := sm.serviceInfo(s) if _, ok := sm.services[info]; ok { panic(fmt.Errorf("service %s already exists, review your code", info)) } sm.services[info] = s } -func (sm *serviceManager) Get(v any) { +// Get retrieves a service of type [v] and assigns it to [v]. +// Panics if a service is not found. +func (sm *ServiceManager) Get(v any) { // Check v is a pointer and implements [Service] to set a value later. t := reflect.TypeOf(v) isPtr := t != nil && t.Kind() == reflect.Pointer @@ -70,12 +71,43 @@ func (sm *serviceManager) Get(v any) { if !isPtr || !stype.Implements(intService) || stype == intService { panic(fmt.Errorf("argument must be a pointer to a type (interface) implementing Service, %q given", t)) } - for _, srv := range sm.services { - st := reflect.TypeOf(srv) - if st.AssignableTo(stype) { - reflect.ValueOf(v).Elem().Set(reflect.ValueOf(srv)) + // Get service by service info. + vsvc := reflect.ValueOf(v).Elem().Interface().(Service) + srv, ok := sm.services[sm.serviceInfo(vsvc)] + if ok && sm.tryToAssignService(srv, v) { + return + } + + // Find the service by type in the registered. + for _, srv = range sm.services { + if sm.tryToAssignService(srv, v) { + return + } + } + + // Try to create and register a service if possible. + if c, ok := vsvc.(ServiceCreate); ok { + newSvc := c.ServiceCreate(sm) + if sm.tryToAssignService(newSvc, v) { + sm.Add(newSvc) return } } panic(fmt.Sprintf("service %q does not exist", stype)) } + +func (sm *ServiceManager) serviceInfo(s Service) ServiceInfo { + info := s.ServiceInfo() + InitServiceInfo(&info, s) + return info +} + +func (sm *ServiceManager) tryToAssignService(s Service, v any) bool { + stype := reflect.TypeOf(v).Elem() + st := reflect.TypeOf(s) + if st.AssignableTo(stype) { + reflect.ValueOf(v).Elem().Set(reflect.ValueOf(s)) + return true + } + return false +} diff --git a/internal/launchr/streams.go b/internal/launchr/streams.go index 1e550e5..3c08731 100644 --- a/internal/launchr/streams.go +++ b/internal/launchr/streams.go @@ -232,7 +232,7 @@ type StreamsModifierFn func(streams *appCli) // WithSensitiveMask decorates streams with a given mask. func WithSensitiveMask(m *SensitiveMask) StreamsModifierFn { return func(streams *appCli) { - streams.out.out = NewMaskingWriter(streams.out.out, m) - streams.err.out = NewMaskingWriter(streams.err.out, m) + streams.out.out = m.MaskWriter(streams.out.out) + streams.err.out = m.MaskWriter(streams.err.out) } } diff --git a/internal/launchr/types.go b/internal/launchr/types.go index 154eebe..54dd205 100644 --- a/internal/launchr/types.go +++ b/internal/launchr/types.go @@ -49,10 +49,12 @@ type App interface { // Deprecated: use app.Services().Get(&v) GetService(v any) // Services returns a service manager. - Services() ServiceManager + Services() *ServiceManager // SensitiveWriter wraps given writer with a sensitive mask. + // Deprecated: use app.Services().Get() SensitiveWriter(w io.Writer) io.Writer // SensitiveMask returns current sensitive mask to add values to mask. + // Deprecated: use app.Services().Get() SensitiveMask() *SensitiveMask // RegisterFS registers a File System in launchr. @@ -192,14 +194,11 @@ func RegisterPlugin(p Plugin) { } // PluginManager handles plugins. -type PluginManager interface { - Service - All() PluginsMap -} +type PluginManager = pluginManagerMap // NewPluginManagerWithRegistered creates [PluginManager] with registered plugins. func NewPluginManagerWithRegistered() PluginManager { - return pluginManagerMap(registeredPlugins) + return registeredPlugins } type pluginManagerMap PluginsMap diff --git a/pkg/action/action.go b/pkg/action/action.go index f887acf..52bfb77 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -21,7 +21,7 @@ type Action struct { // Helpful to reload with replaced variables. loader Loader // services is a service manager to help with Dependency Injection. - services launchr.ServiceManager + services *launchr.ServiceManager // wd is a working directory set from app level. // Usually current working directory, but may be overridden by a plugin. wd string @@ -107,7 +107,7 @@ func (a *Action) Clone() *Action { } // SetServices sets a [launchr.ServiceManager] for Dependency Injection. -func (a *Action) SetServices(s launchr.ServiceManager) { +func (a *Action) SetServices(s *launchr.ServiceManager) { a.services = s } @@ -229,7 +229,7 @@ func (a *Action) EnsureLoaded() (err error) { return err } // Load with replacements. - a.def, err = a.loader.Load(&LoadContext{Action: a, Services: a.services}) + a.def, err = a.loader.Load(&LoadContext{a: a, svc: a.services}) return err } diff --git a/pkg/action/action.input.go b/pkg/action/action.input.go index 3183e16..f2ab6cb 100644 --- a/pkg/action/action.input.go +++ b/pkg/action/action.input.go @@ -205,6 +205,11 @@ func (input *Input) Streams() launchr.Streams { return input.io } +// SetStreams sets IO in input. +func (input *Input) SetStreams(io launchr.Streams) { + input.io = io +} + func (input *Input) execValueProcessors() (err error) { // TODO: Maybe it must run on value change. Need to review how we propagate errors. def := input.action.ActionDef() diff --git a/pkg/action/loader.go b/pkg/action/loader.go index 48c5288..dcf92ac 100644 --- a/pkg/action/loader.go +++ b/pkg/action/loader.go @@ -22,36 +22,48 @@ type Loader interface { // LoadContext stores relevant and isolated data needed for processors. type LoadContext struct { - Action *Action - Services launchr.ServiceManager + a *Action + svc *launchr.ServiceManager tplVars map[string]any tplFuncMap template.FuncMap } -func (ctx *LoadContext) getActionTemplateProcessors() TemplateProcessors { - if ctx.Services == nil { +// Action returns context's action. +func (ctx *LoadContext) Action() *Action { return ctx.a } + +// Services returns a DI container. +func (ctx *LoadContext) Services() *launchr.ServiceManager { return ctx.svc } + +func (ctx *LoadContext) getActionTemplateProcessors() *TemplateProcessors { + if ctx.Services() == nil { + // In case of tests. return NewTemplateProcessors() } - var tp TemplateProcessors - ctx.Services.Get(&tp) + var tp *TemplateProcessors + ctx.Services().Get(&tp) return tp } func (ctx *LoadContext) getTemplateFuncMap() template.FuncMap { if ctx.tplFuncMap == nil { procs := ctx.getActionTemplateProcessors() - ctx.tplFuncMap = procs.GetTemplateFuncMap(TemplateFuncContext{Action: ctx.Action}) + ctx.tplFuncMap = procs.GetTemplateFuncMap( + TemplateFuncContext{ + a: ctx.Action(), + svc: ctx.Services(), + }, + ) } return ctx.tplFuncMap } func (ctx *LoadContext) getTemplateData() map[string]any { if ctx.tplVars == nil { - def := ctx.Action.ActionDef() + def := ctx.Action().ActionDef() // Collect template variables. - ctx.tplVars = convertInputToTplVars(ctx.Action.Input(), def) - addPredefinedVariables(ctx.tplVars, ctx.Action) + ctx.tplVars = convertInputToTplVars(ctx.Action().Input(), def) + addPredefinedVariables(ctx.tplVars, ctx.Action()) } return ctx.tplVars } @@ -85,13 +97,13 @@ func (p *pipeProcessor) Process(ctx *LoadContext, s string) (string, error) { type envProcessor struct{} func (p envProcessor) Process(ctx *LoadContext, s string) (string, error) { - if ctx.Action == nil { + if ctx.Action() == nil { panic("envProcessor received nil LoadContext.Action") } if !strings.Contains(s, "$") { return s, nil } - pv := newPredefinedVars(ctx.Action) + pv := newPredefinedVars(ctx.Action()) getenv := func(key string) string { v, ok := pv.getenv(key) if ok { @@ -120,7 +132,7 @@ func (err errMissingVar) Error() string { } func (p inputProcessor) Process(ctx *LoadContext, s string) (string, error) { - if ctx.Action == nil { + if ctx.Action() == nil { panic("inputProcessor received nil LoadContext.Action") } if !strings.Contains(s, "{{") { diff --git a/pkg/action/loader_test.go b/pkg/action/loader_test.go index 1bac1b2..4fef2c7 100644 --- a/pkg/action/loader_test.go +++ b/pkg/action/loader_test.go @@ -1,6 +1,7 @@ package action import ( + "bytes" "fmt" "os" "testing" @@ -45,21 +46,21 @@ func Test_EnvProcessor(t *testing.T) { _ = os.Setenv("TEST_ENVPROC1", "VAL1") _ = os.Setenv("TEST_ENVPROC2", "VAL2") s := "$TEST_ENVPROC1$TEST_ENVPROC1,${TEST_ENVPROC2},$$TEST_ENVPROC1,${TEST_ENVPROC_UNDEF},${TEST_ENVPROC_UNDEF-$TEST_ENVPROC1},${TEST_ENVPROC_UNDEF:-$TEST_ENVPROC2},${TEST_ENVPROC2+$TEST_ENVPROC1},${TEST_ENVPROC1:+$TEST_ENVPROC2}" - res, err := proc.Process(&LoadContext{Action: act}, s) + res, err := proc.Process(&LoadContext{a: act}, s) assert.NoError(t, err) - assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENVPROC1,,VAL1,VAL2,VAL1,VAL2", string(res)) + assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENVPROC1,,VAL1,VAL2,VAL1,VAL2", res) // Test action predefined env variables. s = "$CBIN,$ACTION_ID,$ACTION_WD,$ACTION_DIR,$DISCOVERY_DIR" - res, err = proc.Process(&LoadContext{Action: act}, s) + res, err = proc.Process(&LoadContext{a: act}, s) exp := fmt.Sprintf("%s,%s,%s,%s,%s", launchr.Executable(), act.ID, act.WorkDir(), act.Dir(), act.fs.Realpath()) assert.NoError(t, err) - assert.Equal(t, exp, string(res)) + assert.Equal(t, exp, res) } func Test_InputProcessor(t *testing.T) { t.Parallel() act := testLoaderAction() - ctx := &LoadContext{Action: act} + ctx := &LoadContext{a: act, svc: launchr.NewServiceManager()} proc := inputProcessor{} input := NewInput(act, InputParams{"arg1": "arg1"}, InputParams{"optStr": "optVal1", "opt-str": "opt-val2"}, nil) input.SetValidated(true) @@ -85,31 +86,71 @@ func Test_InputProcessor(t *testing.T) { assert.Equal(t, errMissVars, err) assert.Equal(t, s, res) - // Remove line if a variable not exists or is nil. + // Test if a variable not exists or is nil. s = `- "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}" - "{{ if (isNil .optUnd) }}optUnd{{ end }}"` res, err = proc.Process(ctx, s) assert.NoError(t, err) assert.Equal(t, "- \"arg1 is not nil\"\n- \"optUnd\"", res) - // Remove line if a variable not exists or is nil, 1 argument is not defined and not checked. - s = `- "{{ .arg2 }}" -- "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}" -- "{{ if (isNil .optUnd) }}optUnd{{ end }}"` - _, err = proc.Process(ctx, s) - assert.Equal(t, errMissVars, err) - + // Test isSet and isChanged. s = `- "{{ if isSet .arg1 }}arg1 is set{{end}}" - "{{ if isChanged .arg1 }}arg1 is changed{{end}}"` res, err = proc.Process(ctx, s) assert.NoError(t, err) assert.Equal(t, "- \"arg1 is set\"\n- \"arg1 is changed\"", res) + + // Prepare a new load context for masked output. + ctx = &LoadContext{a: act, svc: launchr.NewServiceManager()} + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + streams := launchr.NewBasicStreams(nil, outBuf, errBuf) + mySecret := "my_secret_input" + input = NewInput(act, InputParams{"arg1": mySecret}, nil, streams) + input.SetValidated(true) + err = act.SetInput(input) + assert.NoError(t, err) + + // Test we can mask sensitive data in a template when used in output. + s = `{{ .arg1 | mask }}` + res, err = proc.Process(ctx, s) + assert.NoError(t, err) + assert.Equal(t, mySecret, res) + // Test output was masked. + _, _ = act.Input().Streams().Out().Write([]byte(mySecret)) + _, _ = act.Input().Streams().Err().Write([]byte(mySecret)) + assert.Equal(t, "****", outBuf.String()) + assert.Equal(t, "****", errBuf.String()) + // Clean buffer for clean comparison + outBuf.Reset() + errBuf.Reset() + // Test original streams were not affected. + _, _ = streams.Out().Write([]byte(mySecret)) + _, _ = streams.Err().Write([]byte(mySecret)) + assert.Equal(t, mySecret, outBuf.String()) + assert.Equal(t, mySecret, errBuf.String()) + outBuf.Reset() + errBuf.Reset() + + // Test previous run doesn't affect new runs. + s = `{{ .arg1 }}` + input = NewInput(act, InputParams{"arg1": mySecret}, nil, streams) + input.SetValidated(true) + err = act.SetInput(input) + assert.NoError(t, err) + res, err = proc.Process(ctx, s) + assert.NoError(t, err) + assert.Equal(t, mySecret, res) + _, _ = act.Input().Streams().Out().Write([]byte(mySecret)) + _, _ = act.Input().Streams().Err().Write([]byte(mySecret)) + assert.Equal(t, mySecret, outBuf.String()) + assert.Equal(t, mySecret, errBuf.String()) } func Test_PipeProcessor(t *testing.T) { t.Parallel() act := testLoaderAction() - ctx := &LoadContext{Action: act} + ctx := &LoadContext{a: act, svc: launchr.NewServiceManager()} proc := NewPipeProcessor( envProcessor{}, inputProcessor{}, diff --git a/pkg/action/manager.go b/pkg/action/manager.go index 93d9ffe..977e2bd 100644 --- a/pkg/action/manager.go +++ b/pkg/action/manager.go @@ -17,81 +17,10 @@ import ( type DiscoverActionsFn func(ctx context.Context) ([]*Action, error) // Manager handles actions and its execution. -type Manager interface { - launchr.Service - // All returns all actions copied and decorated. - All() map[string]*Action - // Get returns a copy of an action from the manager with default decorators. - Get(id string) (*Action, bool) - // Add saves an action in the manager. - Add(*Action) error - // Delete deletes the action from the manager. - Delete(id string) - - // AddDecorators adds new decorators to manager. - AddDecorators(withFns ...DecorateWithFn) - // Decorate decorates an action with given behaviors. - // If functions withFn are not provided, default functions are applied. - Decorate(a *Action, withFn ...DecorateWithFn) - - // GetIDFromAlias returns a real action ID by its alias. If not, returns alias. - GetIDFromAlias(alias string) string - - // GetActionIDProvider returns global application action id provider. - GetActionIDProvider() IDProvider - // SetActionIDProvider sets global application action id provider. - // This id provider will be used as default on [Action] discovery process. - SetActionIDProvider(p IDProvider) - - // GetPersistentFlags retrieves the instance of FlagsGroup containing global flag definitions and their - // current state. - GetPersistentFlags() *FlagsGroup - - // TemplateProcessors for backward-compatibility. - // Deprecated: Use [TemplateProcessors] service directly. - TemplateProcessors - // SetTemplateProcessors sets [TemplateProcessors] for backward-compatibility. - // Deprecated: no replacement. - SetTemplateProcessors(TemplateProcessors) - - // AddDiscovery registers a discovery callback to find actions. - AddDiscovery(DiscoverActionsFn) - // SetDiscoveryTimeout sets discovery timeout to stop on long-running callbacks. - SetDiscoveryTimeout(timeout time.Duration) - - // ValidateInput validates an action input. - // @todo think about decoupling it from manager to separate service - ValidateInput(a *Action, input *Input) error - - RunManager -} +type Manager = *actionManagerMap // RunManager runs actions and stores runtime information about them. -type RunManager interface { - // Run executes an action in foreground. - Run(ctx context.Context, a *Action) (RunInfo, error) - // RunBackground executes an action in background. - RunBackground(ctx context.Context, a *Action, runID string) (RunInfo, chan error) - // RunInfoByAction returns all running actions by action id. - RunInfoByAction(aid string) []RunInfo - // RunInfoByID returns an action matching run id. - RunInfoByID(id string) (RunInfo, bool) -} - -// ManagerUnsafe is an extension of the [Manager] interface that provides unsafe access to actions. -// Warning: Use this with caution! -type ManagerUnsafe interface { - Manager - // AllUnsafe returns all original action values from the storage. - // Use this method only if you need read-only access to the actions without allocating new memory. - // Warning: It is unsafe to manipulate these actions directly as they are the original instances - // affecting the entire application. - // Normally, for action execution you should use the [Manager.Get] or [Manager.All] methods, - // which provide actions configured for execution. - AllUnsafe() map[string]*Action - // GetUnsafe returns the original action value from the storage. - GetUnsafe(id string) (*Action, bool) -} +type RunManager = *runManagerMap // DecorateWithFn is a type alias for functions accepted in a [Manager.Decorate] interface method. type DecorateWithFn = func(m Manager, a *Action) @@ -111,7 +40,7 @@ type actionManagerMap struct { persistentFlags *FlagsGroup runManagerMap - TemplateProcessors // TODO: It's here to support the interface. Refactor when the interface is updated. + *TemplateProcessors // TODO: It's here for backward compatibility. Refactor when the refs are deleted. } // NewManager constructs a new action manager. @@ -135,6 +64,23 @@ func (m *actionManagerMap) ServiceInfo() launchr.ServiceInfo { return launchr.ServiceInfo{} } +func (m *actionManagerMap) ServiceCreate(svc *launchr.ServiceManager) launchr.Service { + var config launchr.Config + svc.Get(&config) + name := launchr.Version().Name + am := NewManager( + WithDefaultRuntime(config), + WithContainerRuntimeConfig(config, name+"_"), + WithServices(svc), + ) + + var tp *TemplateProcessors + svc.Get(&tp) + am.SetTemplateProcessors(tp) + return am +} + +// Add saves an action in the manager. func (m *actionManagerMap) Add(a *Action) error { m.mx.Lock() defer m.mx.Unlock() @@ -172,6 +118,12 @@ func (m *actionManagerMap) add(a *Action) error { return nil } +// AllUnsafe returns all original action values from the storage. +// Use this method only if you need read-only access to the actions without allocating new memory. +// Warning: It is unsafe to manipulate these actions directly as they are the original instances +// affecting the entire application. +// Normally, for action execution you should use the [Manager.Get] or [Manager.All] methods, +// which provide actions configured for execution. func (m *actionManagerMap) AllUnsafe() map[string]*Action { m.mx.Lock() defer m.mx.Unlock() @@ -181,6 +133,7 @@ func (m *actionManagerMap) AllUnsafe() map[string]*Action { return maps.Clone(m.actionStore) } +// GetIDFromAlias returns a real action ID by its alias. If not, returns alias. func (m *actionManagerMap) GetIDFromAlias(alias string) string { if id, ok := m.actionAliases[alias]; ok { return id @@ -188,6 +141,7 @@ func (m *actionManagerMap) GetIDFromAlias(alias string) string { return alias } +// Delete deletes the action from the manager. func (m *actionManagerMap) Delete(id string) { m.mx.Lock() defer m.mx.Unlock() @@ -203,6 +157,7 @@ func (m *actionManagerMap) Delete(id string) { } } +// All returns all actions copied and decorated. func (m *actionManagerMap) All() map[string]*Action { ret := m.AllUnsafe() for k, v := range ret { @@ -213,6 +168,7 @@ func (m *actionManagerMap) All() map[string]*Action { return ret } +// Get returns a copy of an action from the manager with default decorators. func (m *actionManagerMap) Get(id string) (*Action, bool) { a, ok := m.GetUnsafe(id) // Process action with default decorators and return a copy to have an isolated scope. @@ -221,6 +177,7 @@ func (m *actionManagerMap) Get(id string) (*Action, bool) { return a, ok } +// GetUnsafe returns the original action value from the storage. func (m *actionManagerMap) GetUnsafe(id string) (a *Action, ok bool) { m.mx.Lock() defer m.mx.Unlock() @@ -252,10 +209,12 @@ func (m *actionManagerMap) get(id string) (*Action, bool) { return a, ok } +// SetDiscoveryTimeout sets discovery timeout to stop on long-running callbacks. func (m *actionManagerMap) SetDiscoveryTimeout(timeout time.Duration) { m.discTimeout = timeout } +// AddDiscovery registers a discovery callback to find actions. func (m *actionManagerMap) AddDiscovery(fn DiscoverActionsFn) { if m.discoveryFns == nil { m.discoveryFns = make([]DiscoverActionsFn, 0, 1) @@ -292,14 +251,20 @@ func (m *actionManagerMap) callDiscoveryFn(ctx context.Context, fn DiscoverActio return nil } -func (m *actionManagerMap) SetTemplateProcessors(tp TemplateProcessors) { +// SetTemplateProcessors sets [TemplateProcessors] for backward-compatibility. +// TODO: Remove. +// Deprecated: no replacement. +func (m *actionManagerMap) SetTemplateProcessors(tp *TemplateProcessors) { m.TemplateProcessors = tp } +// AddDecorators adds new decorators to manager. func (m *actionManagerMap) AddDecorators(withFns ...DecorateWithFn) { m.dwFns = append(m.dwFns, withFns...) } +// Decorate decorates an action with given behaviors. +// If functions withFn are not provided, default functions are applied. func (m *actionManagerMap) Decorate(a *Action, withFns ...DecorateWithFn) { if a == nil { return @@ -313,6 +278,7 @@ func (m *actionManagerMap) Decorate(a *Action, withFns ...DecorateWithFn) { } } +// GetActionIDProvider returns global application action id provider. func (m *actionManagerMap) GetActionIDProvider() IDProvider { if m.idProvider == nil { m.SetActionIDProvider(nil) @@ -320,6 +286,8 @@ func (m *actionManagerMap) GetActionIDProvider() IDProvider { return m.idProvider } +// SetActionIDProvider sets global application action id provider. +// This id provider will be used as default on [Action] discovery process. func (m *actionManagerMap) SetActionIDProvider(p IDProvider) { if p == nil { p = DefaultIDProvider{} @@ -327,10 +295,13 @@ func (m *actionManagerMap) SetActionIDProvider(p IDProvider) { m.idProvider = p } +// GetPersistentFlags retrieves the instance of FlagsGroup +// containing global flag definitions and their current state. func (m *actionManagerMap) GetPersistentFlags() *FlagsGroup { return m.persistentFlags } +// ValidateInput validates an action input. func (m *actionManagerMap) ValidateInput(a *Action, input *Input) error { // @todo move to a separate service with full input validation. See notes below. // @todo think about a more elegant solution as right now it forces us to build workarounds for validation. @@ -432,11 +403,13 @@ func (m *runManagerMap) updateRunStatus(id string, st string) { } } +// Run executes an action in foreground. func (m *runManagerMap) Run(ctx context.Context, a *Action) (RunInfo, error) { // @todo add the same status change info return m.registerRun(a, ""), a.Execute(ctx) } +// RunBackground executes an action in background. func (m *runManagerMap) RunBackground(ctx context.Context, a *Action, runID string) (RunInfo, chan error) { // @todo change runID to runOptions with possibility to create filestream names in webUI. ri := m.registerRun(a, runID) @@ -460,6 +433,7 @@ func (m *runManagerMap) RunBackground(ctx context.Context, a *Action, runID stri return ri, chErr } +// RunInfoByAction returns all running actions by action id. func (m *runManagerMap) RunInfoByAction(aid string) []RunInfo { m.mx.Lock() defer m.mx.Unlock() @@ -472,6 +446,7 @@ func (m *runManagerMap) RunInfoByAction(aid string) []RunInfo { return run } +// RunInfoByID returns an action matching run id. func (m *runManagerMap) RunInfoByID(id string) (RunInfo, bool) { m.mx.Lock() defer m.mx.Unlock() @@ -526,7 +501,7 @@ func WithContainerRuntimeConfig(cfg launchr.Config, prefix string) DecorateWithF } // WithServices add global app to action. Helps with DI in actions. -func WithServices(services launchr.ServiceManager) DecorateWithFn { +func WithServices(services *launchr.ServiceManager) DecorateWithFn { return func(_ Manager, a *Action) { a.SetServices(services) } diff --git a/pkg/action/process.go b/pkg/action/process.go index 8ab1722..bcd4dca 100644 --- a/pkg/action/process.go +++ b/pkg/action/process.go @@ -11,30 +11,27 @@ import ( "github.com/launchrctl/launchr/pkg/jsonschema" ) -// TemplateProcessors handles template processors used on an action load. -type TemplateProcessors interface { - launchr.Service - // AddValueProcessor adds processor to list of available processors - AddValueProcessor(name string, vp ValueProcessor) - // GetValueProcessors returns list of available processors - GetValueProcessors() map[string]ValueProcessor - AddTemplateFunc(name string, fn any) - GetTemplateFuncMap(ctx TemplateFuncContext) template.FuncMap -} - // TemplateFuncContext stores context used for processing. type TemplateFuncContext struct { - Action *Action + a *Action + svc *launchr.ServiceManager } -type processManager struct { +// Action returns an [Action] related to the template. +func (ctx TemplateFuncContext) Action() *Action { return ctx.a } + +// Services returns a [launchr.ServiceManager] for DI. +func (ctx TemplateFuncContext) Services() *launchr.ServiceManager { return ctx.svc } + +// TemplateProcessors handles template processors used on an action load. +type TemplateProcessors struct { vproc map[string]ValueProcessor tplFns map[string]any } // NewTemplateProcessors initializes TemplateProcessors with default functions. -func NewTemplateProcessors() TemplateProcessors { - p := &processManager{ +func NewTemplateProcessors() *TemplateProcessors { + p := &TemplateProcessors{ vproc: make(map[string]ValueProcessor), tplFns: make(map[string]any), } @@ -43,7 +40,7 @@ func NewTemplateProcessors() TemplateProcessors { } // defaultTemplateFunc defines template functions available during parsing of an action yaml. -func defaultTemplateFunc(p *processManager) { +func defaultTemplateFunc(p *TemplateProcessors) { // Returns a default value if v is nil or zero. p.AddTemplateFunc("default", func(v, d any) any { // Check IsEmpty method. @@ -82,35 +79,63 @@ func defaultTemplateFunc(p *processManager) { if !ok { return false } - input := ctx.Action.Input() + input := ctx.Action().Input() return input.IsOptChanged(name) || input.IsArgChanged(name) } }) + + // Mask a value in the output in case it's sensitive. + p.AddTemplateFunc("mask", func(ctx TemplateFuncContext) any { + var mask *launchr.SensitiveMask + return func(v string) string { + // Initialize a masking service per action. + if mask == nil { + ctx.Services().Get(&mask) + mask = mask.Clone() + input := ctx.Action().Input() + // TODO: Review. This may not work as expected with Term and Log. + io := input.Streams() + input.SetStreams(launchr.NewBasicStreams(io.In(), io.Out(), io.Err(), launchr.WithSensitiveMask(mask))) + } + mask.AddString(v) + return v + } + }) } -func (m *processManager) ServiceInfo() launchr.ServiceInfo { +// ServiceInfo implements [launchr.Service] interface. +func (m *TemplateProcessors) ServiceInfo() launchr.ServiceInfo { return launchr.ServiceInfo{} } -func (m *processManager) AddValueProcessor(name string, vp ValueProcessor) { +// ServiceCreate implements [launchr.ServiceCreate] interface. +func (m *TemplateProcessors) ServiceCreate(_ *launchr.ServiceManager) launchr.Service { + return NewTemplateProcessors() +} + +// AddValueProcessor adds processor to list of available processors +func (m *TemplateProcessors) AddValueProcessor(name string, vp ValueProcessor) { if _, ok := m.vproc[name]; ok { panic(fmt.Sprintf("value processor %q with the same name already exists", name)) } m.vproc[name] = vp } -func (m *processManager) GetValueProcessors() map[string]ValueProcessor { +// GetValueProcessors returns list of available processors +func (m *TemplateProcessors) GetValueProcessors() map[string]ValueProcessor { return m.vproc } -func (m *processManager) AddTemplateFunc(name string, fn any) { +// AddTemplateFunc registers a template function used on [inputProcessor]. +func (m *TemplateProcessors) AddTemplateFunc(name string, fn any) { if _, ok := m.tplFns[name]; ok { panic(fmt.Sprintf("template function %q with the same name already exists", name)) } m.tplFns[name] = fn } -func (m *processManager) GetTemplateFuncMap(ctx TemplateFuncContext) template.FuncMap { +// GetTemplateFuncMap returns list of template functions used on [inputProcessor]. +func (m *TemplateProcessors) GetTemplateFuncMap(ctx TemplateFuncContext) template.FuncMap { tplFuncMap := template.FuncMap{} for k, v := range m.tplFns { switch v := v.(type) { diff --git a/pkg/action/process_test.go b/pkg/action/process_test.go index db733b0..c83b154 100644 --- a/pkg/action/process_test.go +++ b/pkg/action/process_test.go @@ -153,7 +153,7 @@ type procTestReplaceOptions = *GenericValueProcessorOptions[struct { N string `yaml:"new"` }] -func addTestValueProcessors(tp TemplateProcessors) { +func addTestValueProcessors(tp *TemplateProcessors) { procDefVal := GenericValueProcessor[ValueProcessorOptionsEmpty]{ Fn: func(v any, _ ValueProcessorOptionsEmpty, ctx ValueProcessorContext) (any, error) { if ctx.IsChanged { @@ -190,8 +190,8 @@ func addTestValueProcessors(tp TemplateProcessors) { func Test_ActionsValueProcessor(t *testing.T) { t.Parallel() am := NewManager() - p := NewTemplateProcessors() - addTestValueProcessors(p) + tp := NewTemplateProcessors() + addTestValueProcessors(tp) tt := []TestCaseValueProcessor{ {"valid processor chain - with defaults, input given", actionProcessWithDefault, nil, nil, @@ -214,7 +214,7 @@ func Test_ActionsValueProcessor(t *testing.T) { tt := tt t.Run(tt.Name, func(t *testing.T) { t.Parallel() - tt.Test(t, am, p) + tt.Test(t, am, tp) }) } } diff --git a/pkg/action/test_utils.go b/pkg/action/test_utils.go index 8730b10..e992db6 100644 --- a/pkg/action/test_utils.go +++ b/pkg/action/test_utils.go @@ -98,7 +98,7 @@ type TestCaseValueProcessor struct { } // Test runs the test for [ValueProcessor]. -func (tt TestCaseValueProcessor) Test(t *testing.T, am Manager, tp TemplateProcessors) { +func (tt TestCaseValueProcessor) Test(t *testing.T, am Manager, tp *TemplateProcessors) { a := NewFromYAML(tt.Name, []byte(tt.Yaml)) // Init processors in the action. err := a.setProcessors(tp.GetValueProcessors()) diff --git a/plugins/builtinprocessors/plugin.go b/plugins/builtinprocessors/plugin.go index b9c0293..453420b 100644 --- a/plugins/builtinprocessors/plugin.go +++ b/plugins/builtinprocessors/plugin.go @@ -27,11 +27,11 @@ func (p Plugin) PluginInfo() launchr.PluginInfo { func (p Plugin) OnAppInit(app launchr.App) error { // Get services. var cfg launchr.Config - var am action.Manager + var tp *action.TemplateProcessors app.Services().Get(&cfg) - app.Services().Get(&am) + app.Services().Get(&tp) - addValueProcessors(am, cfg) + addValueProcessors(tp, cfg) // @todo show somehow available processors to developer or document it. @@ -44,7 +44,7 @@ type ConfigGetProcessorOptions = *action.GenericValueProcessorOptions[struct { }] // addValueProcessors submits new [action.ValueProcessor] to [action.Manager]. -func addValueProcessors(tp action.TemplateProcessors, cfg launchr.Config) { +func addValueProcessors(tp *action.TemplateProcessors, cfg launchr.Config) { procCfg := action.GenericValueProcessor[ConfigGetProcessorOptions]{ Fn: func(v any, opts ConfigGetProcessorOptions, ctx action.ValueProcessorContext) (any, error) { return processorConfigGetByKey(v, opts, ctx, cfg) diff --git a/plugins/yamldiscovery/plugin.go b/plugins/yamldiscovery/plugin.go index d8eb252..b6656fb 100644 --- a/plugins/yamldiscovery/plugin.go +++ b/plugins/yamldiscovery/plugin.go @@ -5,6 +5,7 @@ package yamldiscovery import ( "context" "math" + "os" "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/action" @@ -35,6 +36,9 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { func (p *Plugin) OnAppInit(app launchr.App) error { app.Services().Get(&p.am) p.app = app + actionsPath := launchr.MustAbs(launchr.EnvVarActionsPath.Get()) + app.RegisterFS(action.NewDiscoveryFS(os.DirFS(actionsPath), app.GetWD())) + launchr.Log().Debug("init yamldiscovery", "actions_dir", actionsPath) return nil } diff --git a/test/plugins/genaction/go.mod b/test/plugins/genaction/go.mod index 8e8ae07..263452e 100644 --- a/test/plugins/genaction/go.mod +++ b/test/plugins/genaction/go.mod @@ -1,6 +1,6 @@ module example.com/genaction -go 1.24.1 +go 1.25.0 // Have replace for local development replace github.com/launchrctl/launchr => ../../../ diff --git a/test/plugins/testactions/plugin.go b/test/plugins/testactions/plugin.go index 8c99ca3..b1b4414 100644 --- a/test/plugins/testactions/plugin.go +++ b/test/plugins/testactions/plugin.go @@ -36,8 +36,10 @@ func (p *Plugin) OnAppInit(app launchr.App) error { // DiscoverActions implements [launchr.ActionDiscoveryPlugin] interface. func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) { + var mask *launchr.SensitiveMask + p.app.Services().Get(&mask) return []*action.Action{ - actionSensitive(p.app), + actionSensitive(p.app, mask), actionLogLevels(), embedContainerAction(), }, nil diff --git a/test/plugins/testactions/sensitive.go b/test/plugins/testactions/sensitive.go index 564304d..3906d5b 100644 --- a/test/plugins/testactions/sensitive.go +++ b/test/plugins/testactions/sensitive.go @@ -19,16 +19,13 @@ action: required: true ` -func init() { +func actionSensitive(app launchr.App, mask *launchr.SensitiveMask) *action.Action { // Create an action that outputs a secret in a terminal. secret := os.Getenv("TEST_SECRET") if secret != "" { - mask := launchr.GlobalSensitiveMask() mask.AddString(secret) } -} -func actionSensitive(app launchr.App) *action.Action { a := action.NewFromYAML("testplugin:sensitive", []byte(sensitiveYaml)) a.SetRuntime(action.NewFnRuntime(func(_ context.Context, a *action.Action) error { arg := a.Input().Arg("arg").(string) diff --git a/types.go b/types.go index 958fd6a..a2b4bb7 100644 --- a/types.go +++ b/types.go @@ -160,7 +160,7 @@ func NoopStreams() Streams { return launchr.NoopStreams() } func StdInOutErr() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { return launchr.StdInOutErr() } // NewServiceManager initializes ServiceManager. -func NewServiceManager() ServiceManager { return launchr.NewServiceManager() } +func NewServiceManager() *ServiceManager { return launchr.NewServiceManager() } // NewMaskingWriter initializes a new MaskingWriter. func NewMaskingWriter(w io.Writer, mask *SensitiveMask) io.WriteCloser { From 8f551e17e109edafa0cf080835a59d77c9e985a4 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Mon, 29 Sep 2025 22:00:09 +0200 Subject: [PATCH 11/15] Rename template function --- docs/actions.schema.md | 8 ++++---- plugins/builtinprocessors/plugin.go | 12 ++++++------ plugins/builtinprocessors/plugin_test.go | 20 ++++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/actions.schema.md b/docs/actions.schema.md index 52df313..ef5c42c 100644 --- a/docs/actions.schema.md +++ b/docs/actions.schema.md @@ -280,10 +280,10 @@ Or type implements `interface { IsEmpty() bool }`. **Usage:** ```gotemplate -{{ config.Get "foo.bar" }} # retrieves value of any type -{{ index (config.Get "foo.array-elem") 1 }} # retrieves specific array element -{{ config.Get "foo.null-elem" | default "foo" }} # uses default if value is nil -{{ config.Get "foo.missing-elem" | default "bar" }} # uses default if key doesn't exist +{{ config "foo.bar" }} # retrieves value of any type +{{ index (config "foo.array-elem") 1 }} # retrieves specific array element +{{ config "foo.null-elem" | default "foo" }} # uses default if value is nil +{{ config "foo.missing-elem" | default "bar" }} # uses default if key doesn't exist ``` diff --git a/plugins/builtinprocessors/plugin.go b/plugins/builtinprocessors/plugin.go index 453420b..994651a 100644 --- a/plugins/builtinprocessors/plugin.go +++ b/plugins/builtinprocessors/plugin.go @@ -52,7 +52,7 @@ func addValueProcessors(tp *action.TemplateProcessors, cfg launchr.Config) { } tp.AddValueProcessor(procGetConfigValue, procCfg) tplCfg := &configTemplateFunc{cfg: cfg} - tp.AddTemplateFunc("config", func() *configTemplateFunc { return tplCfg }) + tp.AddTemplateFunc("config", tplCfg.Get) } func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.ValueProcessorContext, cfg launchr.Config) (any, error) { @@ -76,7 +76,7 @@ func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.V type configKeyNotFound string // IsEmpty implements a special interface to support "default" template function -// Example: {{ config.Get "foo.bar" | default "buz" }} +// Example: {{ Config "foo.bar" | default "buz" }} func (s configKeyNotFound) IsEmpty() bool { return true } // String implements [fmt.Stringer] to output a missing key to a template. @@ -91,10 +91,10 @@ type configTemplateFunc struct { // // Usage: // -// {{ config.Get "foo.bar" }} - retrieves value of any type -// {{ index (config.Get "foo.array-elem") 1 }} - retrieves specific array element -// {{ config.Get "foo.null-elem" | default "foo" }} - uses default if value is nil -// {{ config.Get "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist +// {{ config "foo.bar" }} - retrieves value of any type +// {{ index (config "foo.array-elem") 1 }} - retrieves specific array element +// {{ config "foo.null-elem" | default "foo" }} - uses default if value is nil +// {{ config "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist func (t *configTemplateFunc) Get(path string) (any, error) { var res any if !t.cfg.Exists(path) { diff --git a/plugins/builtinprocessors/plugin_test.go b/plugins/builtinprocessors/plugin_test.go index 0103cba..21bbc7e 100644 --- a/plugins/builtinprocessors/plugin_test.go +++ b/plugins/builtinprocessors/plugin_test.go @@ -72,13 +72,13 @@ runtime: type: container image: alpine command: - - '{{ config.Get "my.string" }}' - - '{{ config.Get "my.int" }}' - - '{{ config.Get "my.bool" }}' - - '{{ config.Get "my.array" }}' - - '{{ index (config.Get "my.array") 1 }}' - - '{{ config.Get "my.null" | default "foo" }}' - - '{{ config.Get "my.missing" | default "bar" }}' + - '{{ config "my.string" }}' + - '{{ config "my.int" }}' + - '{{ config "my.bool" }}' + - '{{ config "my.array" }}' + - '{{ index (config "my.array") 1 }}' + - '{{ config "my.null" | default "foo" }}' + - '{{ config "my.missing" | default "bar" }}' ` const testTplConfigGetMissing = ` @@ -88,7 +88,7 @@ runtime: type: container image: alpine command: - - '{{ config.Get "my.missing" }}' + - '{{ config "my.missing" }}' ` const testTplConfigGetBadArgs = ` @@ -98,7 +98,7 @@ runtime: type: container image: alpine command: - - '{{ config.Get "my.string" "my.string" }}' + - '{{ config "my.string" "my.string" }}' ` const testConfig = ` @@ -169,7 +169,7 @@ func Test_ConfigTemplateFunc(t *testing.T) { tt := []testCase{ {Name: "valid", Yaml: testTplConfigGet, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}}, {Name: "key not found", Yaml: testTplConfigGetMissing, Exp: []string{""}}, - {Name: "incorrect call", Yaml: testTplConfigGetBadArgs, Err: "wrong number of args for Get: want 1 got 2"}, + {Name: "incorrect call", Yaml: testTplConfigGetBadArgs, Err: "wrong number of args for config: want 1 got 2"}, } for _, tt := range tt { tt := tt From a3235b9158ead48df3aaaab9d58b7b1246092409 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Mon, 29 Sep 2025 23:10:09 +0200 Subject: [PATCH 12/15] Add YQ template func. --- plugins/builtinprocessors/plugin.go | 65 ++++++++++++-- plugins/builtinprocessors/plugin_test.go | 107 +++++++++++++++++++++-- 2 files changed, 160 insertions(+), 12 deletions(-) diff --git a/plugins/builtinprocessors/plugin.go b/plugins/builtinprocessors/plugin.go index 994651a..0ad96bd 100644 --- a/plugins/builtinprocessors/plugin.go +++ b/plugins/builtinprocessors/plugin.go @@ -2,6 +2,14 @@ package builtinprocessors import ( + "fmt" + "os" + "path/filepath" + + "github.com/knadh/koanf" + yamlparser "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/action" "github.com/launchrctl/launchr/pkg/jsonschema" @@ -53,6 +61,10 @@ func addValueProcessors(tp *action.TemplateProcessors, cfg launchr.Config) { tp.AddValueProcessor(procGetConfigValue, procCfg) tplCfg := &configTemplateFunc{cfg: cfg} tp.AddTemplateFunc("config", tplCfg.Get) + tp.AddTemplateFunc("yq", func(ctx action.TemplateFuncContext) any { + tplYq := &yamlQueryTemplateFunc{action: ctx.Action()} + return tplYq.Get + }) } func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.ValueProcessorContext, cfg launchr.Config) (any, error) { @@ -71,16 +83,16 @@ func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.V return jsonschema.EnsureType(ctx.DefParam.Type, res) } -// configKeyNotFound holds a config key element that was not found in config. -// It will print a message in a template when a config key is missing. -type configKeyNotFound string +// tplKeyNotFound holds a key path element that was not found. +// It will print a message in a template when a key is missing. +type tplKeyNotFound string // IsEmpty implements a special interface to support "default" template function // Example: {{ Config "foo.bar" | default "buz" }} -func (s configKeyNotFound) IsEmpty() bool { return true } +func (s tplKeyNotFound) IsEmpty() bool { return true } // String implements [fmt.Stringer] to output a missing key to a template. -func (s configKeyNotFound) String() string { return "" } +func (s tplKeyNotFound) String() string { return "" } // configTemplateFunc is a set of template functions to interact with [launchr.Config] in [action.TemplateProcessors]. type configTemplateFunc struct { @@ -98,7 +110,7 @@ type configTemplateFunc struct { func (t *configTemplateFunc) Get(path string) (any, error) { var res any if !t.cfg.Exists(path) { - return configKeyNotFound(path), nil + return tplKeyNotFound(path), nil } err := t.cfg.Get(path, &res) if err != nil { @@ -106,3 +118,44 @@ func (t *configTemplateFunc) Get(path string) (any, error) { } return res, nil } + +// yamlQueryTemplateFunc is a set of template funciton to parse and query yaml files like `yq`. +type yamlQueryTemplateFunc struct { + action *action.Action +} + +// Get returns a yaml file value by a key path. +// +// Usage: +// +// {{ yq "foo.bar" }} - retrieves value of any type +// {{ index (yq "foo.array-elem") 1 }} - retrieves specific array element +// {{ yq "foo.null-elem" | default "foo" }} - uses default if value is nil +// {{ yq "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist +func (t *yamlQueryTemplateFunc) Get(filename, key string) (any, error) { + k := koanf.New(".") + absPath := filepath.ToSlash(filename) + if !filepath.IsAbs(absPath) { + absPath = filepath.Join(t.action.WorkDir(), absPath) + } + + content, err := os.ReadFile(absPath) //nolint:gosec // G301 File inclusion is expected. + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("can't find yaml file %q", filename) + } + return nil, fmt.Errorf("can't read yaml file %q: %w", filename, err) + } + + err = k.Load(rawbytes.Provider(content), yamlparser.Parser()) + if err != nil { + return nil, err + } + + if !k.Exists(key) { + return tplKeyNotFound(filename + ":" + key), nil + } + + val := k.Get(key) + return val, nil +} diff --git a/plugins/builtinprocessors/plugin_test.go b/plugins/builtinprocessors/plugin_test.go index 21bbc7e..faf7940 100644 --- a/plugins/builtinprocessors/plugin_test.go +++ b/plugins/builtinprocessors/plugin_test.go @@ -1,6 +1,8 @@ package builtinprocessors import ( + "os" + "path/filepath" "testing" "testing/fstest" @@ -65,7 +67,7 @@ action: - processor: config.GetValue ` -const testTplConfigGet = ` +const testTplConfig = ` action: title: test config runtime: @@ -81,7 +83,7 @@ runtime: - '{{ config "my.missing" | default "bar" }}' ` -const testTplConfigGetMissing = ` +const testTplConfigMissing = ` action: title: test config runtime: @@ -91,7 +93,7 @@ runtime: - '{{ config "my.missing" }}' ` -const testTplConfigGetBadArgs = ` +const testTplConfigBadArgs = ` action: title: test config runtime: @@ -110,6 +112,54 @@ my: null: null ` +const testTplYq = ` +action: + title: test yq + options: + - name: yamlPath + default: "foo/bar.yaml" +runtime: + type: container + image: alpine + command: + - '{{ yq .yamlPath "foo.bar" }}' + - '{{ index (yq .yamlPath "foo") "bar" }}' + - '{{ yq .yamlPath "foo.null" | default "foo" }}' + - '{{ yq .yamlPath "my.missing" | default "bar" }}' +` + +const testTplYqMissing = ` +action: + title: test yq + options: + - name: yamlPath + default: "foo/bar.yaml" +runtime: + type: container + image: alpine + command: + - '{{ yq .yamlPath "my.missing" }}' +` + +const testTplYqBadArgs = ` +action: + title: test yq + options: + - name: yamlPath + default: "foo/bar.yaml" +runtime: + type: container + image: alpine + command: + - '{{ yq "1" "2" "3" }}' +` + +const testYqFileContent = ` +foo: + bar: buz + null: null +` + func testConfigFS(s string) launchr.Config { m := fstest.MapFS{ "config.yaml": &fstest.MapFile{Data: []byte(s)}, @@ -167,15 +217,60 @@ func Test_ConfigTemplateFunc(t *testing.T) { } tt := []testCase{ - {Name: "valid", Yaml: testTplConfigGet, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}}, - {Name: "key not found", Yaml: testTplConfigGetMissing, Exp: []string{""}}, - {Name: "incorrect call", Yaml: testTplConfigGetBadArgs, Err: "wrong number of args for config: want 1 got 2"}, + {Name: "valid", Yaml: testTplConfig, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}}, + {Name: "key not found", Yaml: testTplConfigMissing, Exp: []string{""}}, + {Name: "incorrect call", Yaml: testTplConfigBadArgs, Err: "wrong number of args for config: want 1 got 2"}, + } + for _, tt := range tt { + tt := tt + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + a := action.NewFromYAML(tt.Name, []byte(tt.Yaml)) + a.SetServices(svc) + err := a.EnsureLoaded() + if tt.Err != "" { + require.ErrorContains(t, err, tt.Err) + return + } + require.NoError(t, err) + rdef := a.RuntimeDef() + assert.Equal(t, tt.Exp, []string(rdef.Container.Command)) + }) + } +} + +func Test_YqTemplateFunc(t *testing.T) { + // Prepare services. + tp := action.NewTemplateProcessors() + addValueProcessors(tp, nil) + svc := launchr.NewServiceManager() + svc.Add(tp) + + // Prepare test data. + wd := t.TempDir() + err := os.MkdirAll(filepath.Join(wd, "foo"), 0750) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(wd, "foo", "bar.yaml"), []byte(testYqFileContent), 0600) + require.NoError(t, err) + + type testCase struct { + Name string + Yaml string + Exp []string + Err string + } + + tt := []testCase{ + {Name: "valid", Yaml: testTplYq, Exp: []string{"buz", "buz", "foo", "bar"}}, + {Name: "key not found", Yaml: testTplYqMissing, Exp: []string{""}}, + {Name: "incorrect call", Yaml: testTplYqBadArgs, Err: "wrong number of args for yq: want 2 got 3"}, } for _, tt := range tt { tt := tt t.Run(tt.Name, func(t *testing.T) { t.Parallel() a := action.NewFromYAML(tt.Name, []byte(tt.Yaml)) + a.SetWorkDir(wd) a.SetServices(svc) err := a.EnsureLoaded() if tt.Err != "" { From ec068481a0daa0c0b0b5057bdb206b4eb0ccc027 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Tue, 30 Sep 2025 00:18:31 +0200 Subject: [PATCH 13/15] Adjustments. --- pkg/action/action.flags.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/action/action.flags.go b/pkg/action/action.flags.go index cc0fe64..c81ecd8 100644 --- a/pkg/action/action.flags.go +++ b/pkg/action/action.flags.go @@ -25,6 +25,12 @@ func NewFlagsGroup(name string) *FlagsGroup { } } +// GetName returns the name of the flags group. +// Deprecated: use Name(). +func (p *FlagsGroup) GetName() string { + return p.Name() +} + // Name returns the name of the flags group. func (p *FlagsGroup) Name() string { return p.name From fd102770e67ce4cba7ad42aac43b29a2db58f30b Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Tue, 30 Sep 2025 23:46:33 +0200 Subject: [PATCH 14/15] Fix after review. --- internal/launchr/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/launchr/config.go b/internal/launchr/config.go index 1456bf5..f5c6f69 100644 --- a/internal/launchr/config.go +++ b/internal/launchr/config.go @@ -21,7 +21,7 @@ var ( ErrNoConfigFile = errors.New("config file is not found") // ErrNoConfigFile when config file doesn't exist in FS. ) -// Config is a launchr config storage interface. +// Config is a launchr global config service. type Config = *config type cachedProps = map[string]reflect.Value From 9f8c23eef131b4ec76cd4f1e6a63c4b438c9c635 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Wed, 1 Oct 2025 00:55:58 +0200 Subject: [PATCH 15/15] Rename yq to YamlQuery. --- plugins/builtinprocessors/plugin.go | 10 +++++----- plugins/builtinprocessors/plugin_test.go | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/builtinprocessors/plugin.go b/plugins/builtinprocessors/plugin.go index 0ad96bd..66a0803 100644 --- a/plugins/builtinprocessors/plugin.go +++ b/plugins/builtinprocessors/plugin.go @@ -61,7 +61,7 @@ func addValueProcessors(tp *action.TemplateProcessors, cfg launchr.Config) { tp.AddValueProcessor(procGetConfigValue, procCfg) tplCfg := &configTemplateFunc{cfg: cfg} tp.AddTemplateFunc("config", tplCfg.Get) - tp.AddTemplateFunc("yq", func(ctx action.TemplateFuncContext) any { + tp.AddTemplateFunc("YamlQuery", func(ctx action.TemplateFuncContext) any { tplYq := &yamlQueryTemplateFunc{action: ctx.Action()} return tplYq.Get }) @@ -128,10 +128,10 @@ type yamlQueryTemplateFunc struct { // // Usage: // -// {{ yq "foo.bar" }} - retrieves value of any type -// {{ index (yq "foo.array-elem") 1 }} - retrieves specific array element -// {{ yq "foo.null-elem" | default "foo" }} - uses default if value is nil -// {{ yq "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist +// {{ YamlQuery "foo.bar" }} - retrieves value of any type +// {{ index (YamlQuery "foo.array-elem") 1 }} - retrieves specific array element +// {{ YamlQuery "foo.null-elem" | default "foo" }} - uses default if value is nil +// {{ YamlQuery "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist func (t *yamlQueryTemplateFunc) Get(filename, key string) (any, error) { k := koanf.New(".") absPath := filepath.ToSlash(filename) diff --git a/plugins/builtinprocessors/plugin_test.go b/plugins/builtinprocessors/plugin_test.go index faf7940..f4677fc 100644 --- a/plugins/builtinprocessors/plugin_test.go +++ b/plugins/builtinprocessors/plugin_test.go @@ -114,7 +114,7 @@ my: const testTplYq = ` action: - title: test yq + title: test YamlQuery options: - name: yamlPath default: "foo/bar.yaml" @@ -122,15 +122,15 @@ runtime: type: container image: alpine command: - - '{{ yq .yamlPath "foo.bar" }}' - - '{{ index (yq .yamlPath "foo") "bar" }}' - - '{{ yq .yamlPath "foo.null" | default "foo" }}' - - '{{ yq .yamlPath "my.missing" | default "bar" }}' + - '{{ YamlQuery .yamlPath "foo.bar" }}' + - '{{ index (YamlQuery .yamlPath "foo") "bar" }}' + - '{{ YamlQuery .yamlPath "foo.null" | default "foo" }}' + - '{{ YamlQuery .yamlPath "my.missing" | default "bar" }}' ` const testTplYqMissing = ` action: - title: test yq + title: test YamlQuery options: - name: yamlPath default: "foo/bar.yaml" @@ -138,12 +138,12 @@ runtime: type: container image: alpine command: - - '{{ yq .yamlPath "my.missing" }}' + - '{{ YamlQuery .yamlPath "my.missing" }}' ` const testTplYqBadArgs = ` action: - title: test yq + title: test YamlQuery options: - name: yamlPath default: "foo/bar.yaml" @@ -151,7 +151,7 @@ runtime: type: container image: alpine command: - - '{{ yq "1" "2" "3" }}' + - '{{ YamlQuery "1" "2" "3" }}' ` const testYqFileContent = ` @@ -263,7 +263,7 @@ func Test_YqTemplateFunc(t *testing.T) { tt := []testCase{ {Name: "valid", Yaml: testTplYq, Exp: []string{"buz", "buz", "foo", "bar"}}, {Name: "key not found", Yaml: testTplYqMissing, Exp: []string{""}}, - {Name: "incorrect call", Yaml: testTplYqBadArgs, Err: "wrong number of args for yq: want 2 got 3"}, + {Name: "incorrect call", Yaml: testTplYqBadArgs, Err: "wrong number of args for YamlQuery: want 2 got 3"}, } for _, tt := range tt { tt := tt