From a4d6970419fe6de498554692a910a8cf923c81e7 Mon Sep 17 00:00:00 2001 From: Nikolay Mitrofanov Date: Fri, 30 Jan 2026 23:49:11 +0300 Subject: [PATCH 1/6] envs extractor move to pkg/utils package Signed-off-by: Nikolay Mitrofanov --- pkg/ssh/config/parse_flags.go | 128 +------------- pkg/utils/envs_extractor.go | 280 +++++++++++++++++++++++++++++++ pkg/utils/envs_extractor_test.go | 278 ++++++++++++++++++++++++++++++ 3 files changed, 567 insertions(+), 119 deletions(-) create mode 100644 pkg/utils/envs_extractor.go create mode 100644 pkg/utils/envs_extractor_test.go diff --git a/pkg/ssh/config/parse_flags.go b/pkg/ssh/config/parse_flags.go index 837fa66..3405d23 100644 --- a/pkg/ssh/config/parse_flags.go +++ b/pkg/ssh/config/parse_flags.go @@ -21,7 +21,6 @@ import ( "os/user" "path/filepath" "sort" - "strconv" "strings" "github.com/deckhouse/lib-dhctl/pkg/log" @@ -32,6 +31,7 @@ import ( "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/utils" "github.com/deckhouse/lib-connection/pkg/ssh/utils/terminal" + connectionutils "github.com/deckhouse/lib-connection/pkg/utils" ) const ( @@ -84,7 +84,7 @@ type Flags struct { forceNoPrivateKeys bool flagSet *flag.FlagSet - envExtractor *envExtractor + envExtractor *connectionutils.EnvExtractor } func (f *Flags) IsConflictBetweenFlags() error { @@ -231,7 +231,6 @@ func (f *Flags) userExtractor() func() (string, error) { type ( AskPasswordFunc func(promt string) ([]byte, error) - EnvsLookupFunc func(name string) (string, bool) PrivateKeyExtractorFunc func(path string, logger log.Logger) (password string, err error) ) @@ -239,7 +238,7 @@ type FlagsParser struct { envsPrefix string ask AskPasswordFunc sett settings.Settings - envsLookup EnvsLookupFunc + envsLookup connectionutils.EnvsLookupFunc // extractPrivateKey // custom extract content and password for private key file @@ -274,9 +273,7 @@ func NewFlagsParser(sett settings.Settings) *FlagsParser { // This method trim right all _ ang - symbols and spaces left and right // By default parser add _ after prefix for all env vars func (p *FlagsParser) WithEnvsPrefix(envsPrefix string) *FlagsParser { - envsPrefix = strings.TrimSpace(envsPrefix) - envsPrefix = strings.TrimRight(envsPrefix, "_-") - p.envsPrefix = envsPrefix + p.envsPrefix = connectionutils.SimplifyPrefix(envsPrefix) return p } @@ -290,7 +287,7 @@ func (p *FlagsParser) WithAsk(ask AskPasswordFunc) *FlagsParser { return p } -func (p *FlagsParser) WithEnvsLookup(lookup EnvsLookupFunc) *FlagsParser { +func (p *FlagsParser) WithEnvsLookup(lookup connectionutils.EnvsLookupFunc) *FlagsParser { if govalue.Nil(lookup) { p.sett.Logger().WarnF("Envs lookup function is nil. Skip set ask function.") return p @@ -607,8 +604,8 @@ func (p *FlagsParser) ParseFlagsAndExtractConfig(arguments []string, set *flag.F return p.ExtractConfigAfterParse(flags, opts...) } -func (p *FlagsParser) envsExtractor() *envExtractor { - return newEnvExtractor(p.envsPrefix, p.envsLookup) +func (p *FlagsParser) envsExtractor() *connectionutils.EnvExtractor { + return connectionutils.NewEnvExtractor(p.envsPrefix, p.envsLookup) } func (p *FlagsParser) readPrivateKeysFromFlags(flags *Flags, logger log.Logger) ([]AgentPrivateKey, error) { @@ -671,7 +668,7 @@ func (p *FlagsParser) getPasswordsFromUser(flags *Flags) (*passwordsFromUser, er return res, nil } -func getHomeDir(extractor *envExtractor) (string, error) { +func getHomeDir(extractor *connectionutils.EnvExtractor) (string, error) { home := "" extractor.StringWithoutPrefix("HOME", &home) @@ -710,7 +707,7 @@ func getHomeDir(extractor *envExtractor) (string, error) { // returns current user name // first attempt get user from env // can be call multiple times because user.Current() cache user info -func getCurrentUser(extractor *envExtractor) (string, error) { +func getCurrentUser(extractor *connectionutils.EnvExtractor) (string, error) { userName := "" extractor.StringWithoutPrefix("USER", &userName) @@ -750,113 +747,6 @@ func fileReader(path string, fileType string) (io.ReadCloser, error) { return os.Open(fullPath) } -type envExtractor struct { - prefix string - lookupFunc func(string) (string, bool) -} - -func newEnvExtractor(prefix string, lookupFunc EnvsLookupFunc) *envExtractor { - return &envExtractor{ - prefix: prefix, - lookupFunc: lookupFunc, - } -} - -func (e *envExtractor) NameWithPrefix(name string) string { - if e.prefix != "" { - name = fmt.Sprintf("%s_%s", e.prefix, name) - } - - return name -} - -func (e *envExtractor) AddEnvToUsage(usage string, envName string) string { - if envName == "" { - return usage - } - - return fmt.Sprintf("%s (Can rewrite with %s env)", usage, e.NameWithPrefix(envName)) -} - -func (e *envExtractor) Var(name string) (string, bool) { - return e.lookupFunc(e.NameWithPrefix(name)) -} - -func (e *envExtractor) VarWithoutPrefix(name string) (string, bool) { - return e.lookupFunc(name) -} - -func (e *envExtractor) Int(name string, destination *int) (bool, error) { - strVar, ok := e.Var(name) - if !ok { - return false, nil - } - - value, err := strconv.Atoi(strVar) - if err != nil { - return false, fmt.Errorf("Cannot convert '%s' to int for %s: %w", strVar, e.NameWithPrefix(name), err) - } - - *destination = value - - return true, nil -} - -func (e *envExtractor) StringWithoutPrefix(name string, destination *string) bool { - strVar, ok := e.VarWithoutPrefix(name) - if !ok { - return false - } - - *destination = strVar - - return true -} - -func (e *envExtractor) String(name string, destination *string) bool { - strVar, ok := e.Var(name) - if !ok { - return false - } - - *destination = strVar - - return true -} - -func (e *envExtractor) Strings(name string, destination *[]string) bool { - valsStr, ok := e.Var(name) - if !ok { - return false - } - - valsSplit := strings.Split(valsStr, ",") - vals := make([]string, 0, len(valsSplit)) - for _, v := range valsSplit { - if strings.TrimSpace(v) != "" { - vals = append(vals, v) - } - } - - *destination = vals - - return true -} - -// Bool -// returns that env is set -func (e *envExtractor) Bool(name string, destination *bool) bool { - strVar, ok := e.Var(name) - if !ok { - return false - } - value := strVar != "" - - *destination = value - - return true -} - func terminalPrivateKeyPasswordExtractor(path string, defaultPassword []byte, logger log.Logger) (string, error) { _, password, err := utils.ParseSSHPrivateKeyFile(path, string(defaultPassword), logger) diff --git a/pkg/utils/envs_extractor.go b/pkg/utils/envs_extractor.go new file mode 100644 index 0000000..0b3c9a5 --- /dev/null +++ b/pkg/utils/envs_extractor.go @@ -0,0 +1,280 @@ +// Copyright 2026 Flant JSC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/name212/govalue" +) + +type ( + EnvsLookupFunc func(name string) (string, bool) +) + +// SimplifyPrefix +// This method trim right all _ ang - symbols and spaces left and right +func SimplifyPrefix(prefix string) string { + prefix = strings.TrimSpace(prefix) + prefix = strings.TrimRight(prefix, "_-") + + return prefix +} + +type EnvExtractor struct { + prefixSeparator string + sliceSeparator string + prefix string + lookupFunc func(string) (string, bool) +} + +// NewEnvOsExtractor +// create extractor with os.LookupEnv lookup function +func NewEnvOsExtractor(prefix string) *EnvExtractor { + return NewEnvExtractor(prefix, os.LookupEnv) +} + +// NewEnvExtractor +// utils for extract env and pass to destination +// by default extractor use _ for separate prefix and env name +// if need we use WithPrefixSeparator method for set your own or set to empty +// by default slice string extractor split env string by , symbol +// if need we use WithSliceSeparator method for set your own slice separator +func NewEnvExtractor(prefix string, lookupFunc EnvsLookupFunc) *EnvExtractor { + return &EnvExtractor{ + prefixSeparator: "_", + sliceSeparator: ",", + prefix: prefix, + lookupFunc: lookupFunc, + } +} + +// WithSliceSeparator +// set only not empty string +func (e *EnvExtractor) WithSliceSeparator(s string) *EnvExtractor { + if s != "" { + e.sliceSeparator = s + } + + return e +} + +func (e *EnvExtractor) WithPrefixSeparator(s string) *EnvExtractor { + e.prefixSeparator = s + return e +} + +func (e *EnvExtractor) AddEnvToUsage(usage string, envName string) string { + if envName == "" { + return usage + } + + return fmt.Sprintf("%s (Can rewrite with %s env)", usage, e.nameWithPrefix(envName)) +} + +func (e *EnvExtractor) Int(name string, destination *int) (bool, error) { + strVar, ok := e.getVar(name) + if !ok { + return false, nil + } + + value, err := strconv.Atoi(strVar) + if err != nil { + return false, fmt.Errorf("Cannot convert '%s' to int for %s: %w", strVar, e.nameWithPrefix(name), err) + } + + *destination = value + + return true, nil +} + +func (e *EnvExtractor) StringWithoutPrefix(name string, destination *string) bool { + strVar, ok := e.lookupFunc(name) + if !ok { + return false + } + + *destination = strVar + + return true +} + +func (e *EnvExtractor) String(name string, destination *string) bool { + strVar, ok := e.getVar(name) + if !ok { + return false + } + + *destination = strVar + + return true +} + +func (e *EnvExtractor) Strings(name string, destination *[]string) bool { + valsStr, ok := e.getVar(name) + if !ok { + return false + } + + valsSplit := strings.Split(valsStr, e.sliceSeparator) + vals := make([]string, 0, len(valsSplit)) + for _, v := range valsSplit { + if strings.TrimSpace(v) != "" { + vals = append(vals, v) + } + } + + *destination = vals + + return true +} + +// Bool +// returns that env is set +func (e *EnvExtractor) Bool(name string, destination *bool) bool { + strVar, ok := e.getVar(name) + if !ok { + return false + } + value := strVar != "" + + *destination = value + + return true +} + +func (e *EnvExtractor) nameWithPrefix(name string) string { + if e.prefix != "" { + name = fmt.Sprintf("%s%s%s", e.prefix, e.prefixSeparator, name) + } + + return name +} + +func (e *EnvExtractor) getVar(name string) (string, bool) { + return e.lookupFunc(e.nameWithPrefix(name)) +} + +type EnvVar struct { + Destination any + Present bool +} + +func NewEnvVar(destination any) *EnvVar { + return &EnvVar{ + Destination: destination, + } +} + +type EnvVarsMap = map[string]*EnvVar + +// ExtractAll +// extract all envs from map +// if env present but have empty value set EnvVar Present field to true +// you can process it if need +// ExtractAll found need type for Destination, but destination should be pointer +// Warning! if error returned some Destination can be set +func (e *EnvExtractor) ExtractAll(envsToPointers EnvVarsMap) error { + var errs *multierror.Error + appendError := func(envName string, msg string) { + errs = multierror.Append(errs, fmt.Errorf("%s for env variable '%s'", msg, envName)) + } + + for name, val := range envsToPointers { + valAny := val.Destination + + if govalue.Nil(valAny) { + appendError(name, "value is nil") + continue + } + + v := reflect.ValueOf(valAny) + + if v.Kind() != reflect.Ptr { + appendError(name, "value should be pointer") + continue + } + + if v.IsNil() { + appendError(name, "value is nil") + continue + } + + elem := v.Elem() + kind := v.Type().Elem().Kind() + switch kind { + case reflect.Int: + var destInt int + + present, err := e.Int(name, &destInt) + if err != nil { + appendError(name, err.Error()) + continue + } + + elem.SetInt(int64(destInt)) + val.Present = present + case reflect.String: + strDest := "" + val.Present = e.String(name, &strDest) + elem.SetString(strDest) + case reflect.Bool: + var destBool bool + val.Present = e.Bool(name, &destBool) + elem.SetBool(destBool) + case reflect.Slice: + if err := e.processSlice(name, elem, val); err != "" { + appendError(name, err) + } + default: + appendError(name, incorrectValErr(kind, false)) + } + } + + return errs.ErrorOrNil() +} + +func (e *EnvExtractor) processSlice(name string, slice reflect.Value, val *EnvVar) string { + kind := slice.Type().Elem().Kind() + switch kind { + case reflect.String: + var destStrSlice []string + val.Present = e.Strings(name, &destStrSlice) + slice.Set(reflect.ValueOf(destStrSlice)) + default: + return incorrectValErr(kind, true) + } + + return "" +} + +func incorrectValErr(kind reflect.Kind, isSlice bool) string { + msg := []string{ + "incorrect value", + } + + if isSlice { + msg = append(msg, "slice") + } + + msg = append(msg, fmt.Sprintf("pointer type '%s'. Should be int, string, bool or []string", kind)) + + return strings.Join(msg, " ") +} diff --git a/pkg/utils/envs_extractor_test.go b/pkg/utils/envs_extractor_test.go new file mode 100644 index 0000000..946e875 --- /dev/null +++ b/pkg/utils/envs_extractor_test.go @@ -0,0 +1,278 @@ +// Copyright 2026 Flant JSC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnvExtractorExtractAll(t *testing.T) { + type someStruct struct { + Bool bool + String string + Int int + StringSlice []string + } + + prefix := "MY" + + appendPrefix := func(envs map[string]string) map[string]string { + res := make(map[string]string, len(envs)) + + for k, v := range envs { + res[fmt.Sprintf("%s_%s", prefix, k)] = v + } + + return res + } + + const ( + BoolEnv = "BOOL_VAR" + StringEnv = "STRING_VAR" + IntEnv = "INT_VAR" + SliceEnv = "SLICE_VAR" + ) + + getExtractor := func(envs map[string]string) *EnvExtractor { + return NewEnvExtractor(prefix, func(name string) (string, bool) { + value, ok := envs[name] + return value, ok + }) + } + + assertErr := func(e error, contains ...string) { + if len(contains) == 0 { + require.NoError(t, e, "should not fail") + return + } + + require.Error(t, e, "should fail") + for _, c := range contains { + require.Contains(t, e.Error(), c, "error should contains") + } + } + + t.Run("not parsed", func(t *testing.T) { + envsForErrorCases := appendPrefix(map[string]string{ + StringEnv: "incorrect", + IntEnv: "incorrect", + }) + + t.Run("has nil", func(t *testing.T) { + var str string + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: NewEnvVar(&str), + IntEnv: NewEnvVar(nil), + }) + + assertErr(err, "value is nil for env variable 'INT_VAR'") + }) + + t.Run("has not ptr", func(t *testing.T) { + var str string + var i int + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: NewEnvVar(&str), + IntEnv: NewEnvVar(i), + }) + + assertErr(err, "value should be pointer for env variable 'INT_VAR'") + }) + + t.Run("has not supported type", func(t *testing.T) { + type myStruct struct { + Var int + } + + t.Run("scalar", func(t *testing.T) { + var str string + var my myStruct + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: NewEnvVar(&str), + IntEnv: NewEnvVar(&my), + }) + + assertErr(err, "incorrect value pointer type 'struct'. Should be int, string, bool or []string for env variable 'INT_VAR'") + }) + + t.Run("slice", func(t *testing.T) { + var str string + var my []myStruct + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: NewEnvVar(&str), + IntEnv: NewEnvVar(&my), + }) + + assertErr(err, "incorrect value slice pointer type 'struct'. Should be int, string, bool or []string for env variable 'INT_VAR'") + }) + }) + + t.Run("incorrect int", func(t *testing.T) { + dest := someStruct{} + + extractor := getExtractor(appendPrefix(map[string]string{ + IntEnv: "1not int", + })) + + err := extractor.ExtractAll(EnvVarsMap{ + IntEnv: NewEnvVar(&dest.Int), + }) + + assertErr(err, "Cannot convert '1not int' to int for MY_INT_VAR") + }) + + t.Run("all errors present", func(t *testing.T) { + var i int + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: NewEnvVar(nil), + IntEnv: NewEnvVar(i), + }) + + assertErr( + err, + "value is nil for env variable 'STRING_VAR'", + "value should be pointer for env variable 'INT_VAR'", + ) + }) + }) + + t.Run("parsed", func(t *testing.T) { + t.Run("all", func(t *testing.T) { + dest := someStruct{} + + extractor := getExtractor(appendPrefix(map[string]string{ + StringEnv: "my string", + IntEnv: "22", + BoolEnv: "true", + SliceEnv: "first,second,third", + })) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: NewEnvVar(&dest.String), + IntEnv: NewEnvVar(&dest.Int), + BoolEnv: NewEnvVar(&dest.Bool), + SliceEnv: NewEnvVar(&dest.StringSlice), + }) + + assertErr(err) + + require.Equal(t, "my string", dest.String, "should set val") + require.Equal(t, 22, dest.Int, "should set val") + require.True(t, dest.Bool, "should set val") + require.Equal(t, []string{"first", "second", "third"}, dest.StringSlice, "should set val") + }) + + t.Run("partly set present", func(t *testing.T) { + dest := someStruct{} + + extractor := getExtractor(appendPrefix(map[string]string{ + BoolEnv: "", + SliceEnv: "", + })) + + presentVals := make([]*EnvVar, 0, 2) + + boolVal := NewEnvVar(&dest.Bool) + presentVals = append(presentVals, boolVal) + + sliceVal := NewEnvVar(&dest.StringSlice) + presentVals = append(presentVals, sliceVal) + + notPresentsVals := make([]*EnvVar, 0, 2) + + stringVal := NewEnvVar(&dest.String) + notPresentsVals = append(notPresentsVals, stringVal) + + intVal := NewEnvVar(&dest.Int) + notPresentsVals = append(notPresentsVals, intVal) + + err := extractor.ExtractAll(EnvVarsMap{ + StringEnv: stringVal, + IntEnv: intVal, + BoolEnv: boolVal, + SliceEnv: sliceVal, + }) + + assertErr(err) + + for _, val := range presentVals { + require.True(t, val.Present, "should set present") + } + + for _, val := range notPresentsVals { + require.False(t, val.Present, "should not set present") + } + + require.Empty(t, dest.String, "should not set val") + require.Equal(t, 0, dest.Int, "should not set val") + require.False(t, dest.Bool, "should set val") + require.Equal(t, make([]string, 0), dest.StringSlice, "should set val") + }) + + t.Run("with custom slice separator", func(t *testing.T) { + dest := someStruct{} + + extractor := getExtractor(appendPrefix(map[string]string{ + SliceEnv: "first;second;third", + })).WithSliceSeparator(";") + + err := extractor.ExtractAll(EnvVarsMap{ + SliceEnv: NewEnvVar(&dest.StringSlice), + }) + + assertErr(err) + + require.Equal(t, []string{"first", "second", "third"}, dest.StringSlice, "should set val") + }) + + t.Run("with custom prefix separator", func(t *testing.T) { + dest := someStruct{} + + extractor := getExtractor(map[string]string{ + prefix + IntEnv: "-22", + }).WithPrefixSeparator("") + + err := extractor.ExtractAll(EnvVarsMap{ + IntEnv: NewEnvVar(&dest.Int), + }) + + assertErr(err) + + require.Equal(t, -22, dest.Int, "should set val") + }) + }) +} + +func TestSimplifyPrefix(t *testing.T) { + require.Equal(t, "MY", SimplifyPrefix(" MY_")) +} From f9096dc17aac8f4b716596637deb8721ac4f7317 Mon Sep 17 00:00:00 2001 From: Nikolay Mitrofanov Date: Sat, 31 Jan 2026 00:08:46 +0300 Subject: [PATCH 2/6] add test/no-integration to make file Signed-off-by: Nikolay Mitrofanov --- pkg/utils/envs_extractor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/envs_extractor.go b/pkg/utils/envs_extractor.go index 0b3c9a5..6670350 100644 --- a/pkg/utils/envs_extractor.go +++ b/pkg/utils/envs_extractor.go @@ -30,7 +30,7 @@ type ( ) // SimplifyPrefix -// This method trim right all _ ang - symbols and spaces left and right +// This method trim right all _ ang - symbols and spaces left and right. func SimplifyPrefix(prefix string) string { prefix = strings.TrimSpace(prefix) prefix = strings.TrimRight(prefix, "_-") From fa37c90afdc05e26b22fff575ff5441cdcef4d3e Mon Sep 17 00:00:00 2001 From: Nikolay Mitrofanov Date: Sat, 31 Jan 2026 19:35:59 +0300 Subject: [PATCH 3/6] add test/no-integration to make file Signed-off-by: Nikolay Mitrofanov --- .../{envs_extractor.go => env/extractor.go} | 91 ++++++++---- .../extractor_test.go} | 140 +++++++++++------- 2 files changed, 147 insertions(+), 84 deletions(-) rename pkg/utils/{envs_extractor.go => env/extractor.go} (73%) rename pkg/utils/{envs_extractor_test.go => env/extractor_test.go} (68%) diff --git a/pkg/utils/envs_extractor.go b/pkg/utils/env/extractor.go similarity index 73% rename from pkg/utils/envs_extractor.go rename to pkg/utils/env/extractor.go index 6670350..279c876 100644 --- a/pkg/utils/envs_extractor.go +++ b/pkg/utils/env/extractor.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package utils +package env import ( "fmt" @@ -38,27 +38,27 @@ func SimplifyPrefix(prefix string) string { return prefix } -type EnvExtractor struct { +type Extractor struct { prefixSeparator string sliceSeparator string prefix string lookupFunc func(string) (string, bool) } -// NewEnvOsExtractor +// NewOsExtractor // create extractor with os.LookupEnv lookup function -func NewEnvOsExtractor(prefix string) *EnvExtractor { - return NewEnvExtractor(prefix, os.LookupEnv) +func NewOsExtractor(prefix string) *Extractor { + return NewExtractor(prefix, os.LookupEnv) } -// NewEnvExtractor +// NewExtractor // utils for extract env and pass to destination // by default extractor use _ for separate prefix and env name // if need we use WithPrefixSeparator method for set your own or set to empty // by default slice string extractor split env string by , symbol // if need we use WithSliceSeparator method for set your own slice separator -func NewEnvExtractor(prefix string, lookupFunc EnvsLookupFunc) *EnvExtractor { - return &EnvExtractor{ +func NewExtractor(prefix string, lookupFunc EnvsLookupFunc) *Extractor { + return &Extractor{ prefixSeparator: "_", sliceSeparator: ",", prefix: prefix, @@ -68,7 +68,7 @@ func NewEnvExtractor(prefix string, lookupFunc EnvsLookupFunc) *EnvExtractor { // WithSliceSeparator // set only not empty string -func (e *EnvExtractor) WithSliceSeparator(s string) *EnvExtractor { +func (e *Extractor) WithSliceSeparator(s string) *Extractor { if s != "" { e.sliceSeparator = s } @@ -76,12 +76,12 @@ func (e *EnvExtractor) WithSliceSeparator(s string) *EnvExtractor { return e } -func (e *EnvExtractor) WithPrefixSeparator(s string) *EnvExtractor { +func (e *Extractor) WithPrefixSeparator(s string) *Extractor { e.prefixSeparator = s return e } -func (e *EnvExtractor) AddEnvToUsage(usage string, envName string) string { +func (e *Extractor) AddEnvToUsage(usage string, envName string) string { if envName == "" { return usage } @@ -89,7 +89,7 @@ func (e *EnvExtractor) AddEnvToUsage(usage string, envName string) string { return fmt.Sprintf("%s (Can rewrite with %s env)", usage, e.nameWithPrefix(envName)) } -func (e *EnvExtractor) Int(name string, destination *int) (bool, error) { +func (e *Extractor) Int(name string, destination *int) (bool, error) { strVar, ok := e.getVar(name) if !ok { return false, nil @@ -105,7 +105,7 @@ func (e *EnvExtractor) Int(name string, destination *int) (bool, error) { return true, nil } -func (e *EnvExtractor) StringWithoutPrefix(name string, destination *string) bool { +func (e *Extractor) StringWithoutPrefix(name string, destination *string) bool { strVar, ok := e.lookupFunc(name) if !ok { return false @@ -116,7 +116,7 @@ func (e *EnvExtractor) StringWithoutPrefix(name string, destination *string) boo return true } -func (e *EnvExtractor) String(name string, destination *string) bool { +func (e *Extractor) String(name string, destination *string) bool { strVar, ok := e.getVar(name) if !ok { return false @@ -127,7 +127,7 @@ func (e *EnvExtractor) String(name string, destination *string) bool { return true } -func (e *EnvExtractor) Strings(name string, destination *[]string) bool { +func (e *Extractor) Strings(name string, destination *[]string) bool { valsStr, ok := e.getVar(name) if !ok { return false @@ -148,7 +148,7 @@ func (e *EnvExtractor) Strings(name string, destination *[]string) bool { // Bool // returns that env is set -func (e *EnvExtractor) Bool(name string, destination *bool) bool { +func (e *Extractor) Bool(name string, destination *bool) bool { strVar, ok := e.getVar(name) if !ok { return false @@ -160,7 +160,7 @@ func (e *EnvExtractor) Bool(name string, destination *bool) bool { return true } -func (e *EnvExtractor) nameWithPrefix(name string) string { +func (e *Extractor) nameWithPrefix(name string) string { if e.prefix != "" { name = fmt.Sprintf("%s%s%s", e.prefix, e.prefixSeparator, name) } @@ -168,36 +168,53 @@ func (e *EnvExtractor) nameWithPrefix(name string) string { return name } -func (e *EnvExtractor) getVar(name string) (string, bool) { +func (e *Extractor) getVar(name string) (string, bool) { return e.lookupFunc(e.nameWithPrefix(name)) } -type EnvVar struct { +type Var struct { + Name string Destination any Present bool } -func NewEnvVar(destination any) *EnvVar { - return &EnvVar{ +func NewVar(name string, destination any) *Var { + return &Var{ + Name: name, Destination: destination, } } -type EnvVarsMap = map[string]*EnvVar - -// ExtractAll -// extract all envs from map -// if env present but have empty value set EnvVar Present field to true -// you can process it if need -// ExtractAll found need type for Destination, but destination should be pointer -// Warning! if error returned some Destination can be set -func (e *EnvExtractor) ExtractAll(envsToPointers EnvVarsMap) error { +// ExtractAllVars +// same as ExtractAll but can pass as variadic arguments +func (e *Extractor) ExtractAllVars(vars ...*Var) error { var errs *multierror.Error appendError := func(envName string, msg string) { errs = multierror.Append(errs, fmt.Errorf("%s for env variable '%s'", msg, envName)) } - for name, val := range envsToPointers { + names := make(map[string]int, len(vars)) + for _, v := range vars { + names[v.Name]++ + } + + for name, count := range names { + if count > 1 { + appendError(name, fmt.Sprintf("have multiple names %d", count)) + } + } + + if err := errs.ErrorOrNil(); err != nil { + return err + } + + for i, val := range vars { + name := val.Name + if name == "" { + appendError(fmt.Sprintf("vars[%d]", i), "name is empty") + continue + } + valAny := val.Destination if govalue.Nil(valAny) { @@ -251,7 +268,17 @@ func (e *EnvExtractor) ExtractAll(envsToPointers EnvVarsMap) error { return errs.ErrorOrNil() } -func (e *EnvExtractor) processSlice(name string, slice reflect.Value, val *EnvVar) string { +// ExtractAll +// extract all envs from map +// if env present but have empty value set Var Present field to true +// you can process it if need +// ExtractAll found need type for Destination, but destination should be pointer +// Warning! if error returned some Destination can be set +func (e *Extractor) ExtractAll(vars []*Var) error { + return e.ExtractAllVars(vars...) +} + +func (e *Extractor) processSlice(name string, slice reflect.Value, val *Var) string { kind := slice.Type().Elem().Kind() switch kind { case reflect.String: diff --git a/pkg/utils/envs_extractor_test.go b/pkg/utils/env/extractor_test.go similarity index 68% rename from pkg/utils/envs_extractor_test.go rename to pkg/utils/env/extractor_test.go index 946e875..d0dc047 100644 --- a/pkg/utils/envs_extractor_test.go +++ b/pkg/utils/env/extractor_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package utils +package env import ( "fmt" @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestEnvExtractorExtractAll(t *testing.T) { +func TestExtractAll(t *testing.T) { type someStruct struct { Bool bool String string @@ -48,8 +48,8 @@ func TestEnvExtractorExtractAll(t *testing.T) { SliceEnv = "SLICE_VAR" ) - getExtractor := func(envs map[string]string) *EnvExtractor { - return NewEnvExtractor(prefix, func(name string) (string, bool) { + getExtractor := func(envs map[string]string) *Extractor { + return NewExtractor(prefix, func(name string) (string, bool) { value, ok := envs[name] return value, ok }) @@ -73,15 +73,51 @@ func TestEnvExtractorExtractAll(t *testing.T) { IntEnv: "incorrect", }) + t.Run("has empty name", func(t *testing.T) { + var str string + var i int + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAllVars( + NewVar(StringEnv, &str), + NewVar("", &i), + ) + + assertErr(err, "name is empty for env variable 'vars[1]'") + }) + + t.Run("not unique names", func(t *testing.T) { + dest := someStruct{} + + extractor := getExtractor(envsForErrorCases) + + err := extractor.ExtractAllVars( + NewVar(StringEnv, &dest.String), + NewVar(BoolEnv, &dest.Bool), + NewVar(StringEnv, &dest.String), + NewVar(IntEnv, &dest.Int), + NewVar(IntEnv, &dest.Int), + NewVar(SliceEnv, &dest.StringSlice), + NewVar(IntEnv, &dest.Int), + ) + + assertErr( + err, + "have multiple names 2 for env variable 'STRING_VAR'", + "have multiple names 3 for env variable 'INT_VAR'", + ) + }) + t.Run("has nil", func(t *testing.T) { var str string extractor := getExtractor(envsForErrorCases) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: NewEnvVar(&str), - IntEnv: NewEnvVar(nil), - }) + err := extractor.ExtractAllVars( + NewVar(StringEnv, &str), + NewVar(IntEnv, nil), + ) assertErr(err, "value is nil for env variable 'INT_VAR'") }) @@ -92,10 +128,10 @@ func TestEnvExtractorExtractAll(t *testing.T) { extractor := getExtractor(envsForErrorCases) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: NewEnvVar(&str), - IntEnv: NewEnvVar(i), - }) + err := extractor.ExtractAllVars( + NewVar(StringEnv, &str), + NewVar(IntEnv, i), + ) assertErr(err, "value should be pointer for env variable 'INT_VAR'") }) @@ -111,10 +147,10 @@ func TestEnvExtractorExtractAll(t *testing.T) { extractor := getExtractor(envsForErrorCases) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: NewEnvVar(&str), - IntEnv: NewEnvVar(&my), - }) + err := extractor.ExtractAllVars( + NewVar(StringEnv, &str), + NewVar(IntEnv, &my), + ) assertErr(err, "incorrect value pointer type 'struct'. Should be int, string, bool or []string for env variable 'INT_VAR'") }) @@ -125,10 +161,10 @@ func TestEnvExtractorExtractAll(t *testing.T) { extractor := getExtractor(envsForErrorCases) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: NewEnvVar(&str), - IntEnv: NewEnvVar(&my), - }) + err := extractor.ExtractAllVars( + NewVar(StringEnv, &str), + NewVar(IntEnv, &my), + ) assertErr(err, "incorrect value slice pointer type 'struct'. Should be int, string, bool or []string for env variable 'INT_VAR'") }) @@ -141,9 +177,9 @@ func TestEnvExtractorExtractAll(t *testing.T) { IntEnv: "1not int", })) - err := extractor.ExtractAll(EnvVarsMap{ - IntEnv: NewEnvVar(&dest.Int), - }) + err := extractor.ExtractAllVars( + NewVar(IntEnv, &dest.Int), + ) assertErr(err, "Cannot convert '1not int' to int for MY_INT_VAR") }) @@ -153,10 +189,10 @@ func TestEnvExtractorExtractAll(t *testing.T) { extractor := getExtractor(envsForErrorCases) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: NewEnvVar(nil), - IntEnv: NewEnvVar(i), - }) + err := extractor.ExtractAllVars( + NewVar(StringEnv, nil), + NewVar(IntEnv, i), + ) assertErr( err, @@ -177,12 +213,12 @@ func TestEnvExtractorExtractAll(t *testing.T) { SliceEnv: "first,second,third", })) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: NewEnvVar(&dest.String), - IntEnv: NewEnvVar(&dest.Int), - BoolEnv: NewEnvVar(&dest.Bool), - SliceEnv: NewEnvVar(&dest.StringSlice), - }) + err := extractor.ExtractAllVars( + NewVar(StringEnv, &dest.String), + NewVar(IntEnv, &dest.Int), + NewVar(BoolEnv, &dest.Bool), + NewVar(SliceEnv, &dest.StringSlice), + ) assertErr(err) @@ -200,37 +236,37 @@ func TestEnvExtractorExtractAll(t *testing.T) { SliceEnv: "", })) - presentVals := make([]*EnvVar, 0, 2) + presentVals := make([]*Var, 0, 2) - boolVal := NewEnvVar(&dest.Bool) + boolVal := NewVar(BoolEnv, &dest.Bool) presentVals = append(presentVals, boolVal) - sliceVal := NewEnvVar(&dest.StringSlice) + sliceVal := NewVar(SliceEnv, &dest.StringSlice) presentVals = append(presentVals, sliceVal) - notPresentsVals := make([]*EnvVar, 0, 2) + notPresentsVals := make([]*Var, 0, 2) - stringVal := NewEnvVar(&dest.String) + stringVal := NewVar(StringEnv, &dest.String) notPresentsVals = append(notPresentsVals, stringVal) - intVal := NewEnvVar(&dest.Int) + intVal := NewVar(IntEnv, &dest.Int) notPresentsVals = append(notPresentsVals, intVal) - err := extractor.ExtractAll(EnvVarsMap{ - StringEnv: stringVal, - IntEnv: intVal, - BoolEnv: boolVal, - SliceEnv: sliceVal, - }) + err := extractor.ExtractAllVars( + stringVal, + intVal, + boolVal, + sliceVal, + ) assertErr(err) for _, val := range presentVals { - require.True(t, val.Present, "should set present") + require.True(t, val.Present, "should set present for %s", val.Name) } for _, val := range notPresentsVals { - require.False(t, val.Present, "should not set present") + require.False(t, val.Present, "should not set present for %s", val.Name) } require.Empty(t, dest.String, "should not set val") @@ -246,8 +282,8 @@ func TestEnvExtractorExtractAll(t *testing.T) { SliceEnv: "first;second;third", })).WithSliceSeparator(";") - err := extractor.ExtractAll(EnvVarsMap{ - SliceEnv: NewEnvVar(&dest.StringSlice), + err := extractor.ExtractAll([]*Var{ + NewVar(SliceEnv, &dest.StringSlice), }) assertErr(err) @@ -262,9 +298,9 @@ func TestEnvExtractorExtractAll(t *testing.T) { prefix + IntEnv: "-22", }).WithPrefixSeparator("") - err := extractor.ExtractAll(EnvVarsMap{ - IntEnv: NewEnvVar(&dest.Int), - }) + err := extractor.ExtractAllVars( + NewVar(IntEnv, &dest.Int), + ) assertErr(err) From 046df0491b30b8a8ebc5b9864ca6abc30a967005 Mon Sep 17 00:00:00 2001 From: Nikolay Mitrofanov Date: Sat, 31 Jan 2026 20:00:16 +0300 Subject: [PATCH 4/6] add test/no-integration to make file Signed-off-by: Nikolay Mitrofanov --- pkg/ssh/config/parse_flags.go | 67 ++++++++++++++++----------------- pkg/utils/env/extractor.go | 18 ++++++--- pkg/utils/env/extractor_test.go | 25 ++++++++++++ 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/pkg/ssh/config/parse_flags.go b/pkg/ssh/config/parse_flags.go index 3405d23..fba948e 100644 --- a/pkg/ssh/config/parse_flags.go +++ b/pkg/ssh/config/parse_flags.go @@ -31,7 +31,7 @@ import ( "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/utils" "github.com/deckhouse/lib-connection/pkg/ssh/utils/terminal" - connectionutils "github.com/deckhouse/lib-connection/pkg/utils" + "github.com/deckhouse/lib-connection/pkg/utils/env" ) const ( @@ -84,7 +84,7 @@ type Flags struct { forceNoPrivateKeys bool flagSet *flag.FlagSet - envExtractor *connectionutils.EnvExtractor + envExtractor *env.Extractor } func (f *Flags) IsConflictBetweenFlags() error { @@ -152,38 +152,35 @@ func (f *Flags) RewriteFromEnvs() error { return notInitializedError("envExtractor") } - f.envExtractor.Bool(ForceNoPrivateKeysEnv, &f.forceNoPrivateKeys) - - if !f.forceNoPrivateKeys { - isSet := f.envExtractor.Strings(AgentPrivateKeysEnv, &f.PrivateKeysPaths) - - if isSet && len(f.PrivateKeysPaths) == 0 { - f.forceNoPrivateKeys = true - } - } + privateKeysVal := env.NewVar(AgentPrivateKeysEnv, &f.PrivateKeysPaths) + + err := f.envExtractor.ExtractAllVars( + env.NewVar(BastionPortEnv, &f.BastionPort), + env.NewVar(PortEnv, &f.Port), + env.NewVar(ForceNoPrivateKeysEnv, &f.forceNoPrivateKeys), + privateKeysVal, + env.NewVar(BastionHostEnv, &f.BastionHost), + env.NewVar(BastionUserEnv, &f.BastionUser), + env.NewVar(UserEnv, &f.User), + env.NewVar(HostsEnv, &f.Hosts), + env.NewVar(ExtraArgsEnv, &f.ExtraArgs), + env.NewVar(ConnectionConfigEnv, &f.ConnectionConfigPath), + env.NewVar(LegacyModeEnv, &f.ForceLegacy), + env.NewVar(ModernModeEnv, &f.ForceModern), + env.NewVar(AskBastionPasswordEnv, &f.AskBastionPass), + env.NewVar(AskSudoPasswordEnv, &f.AskSudoPass), + ) - f.envExtractor.String(BastionHostEnv, &f.BastionHost) - f.envExtractor.String(BastionUserEnv, &f.BastionUser) - if _, err := f.envExtractor.Int(BastionPortEnv, &f.BastionPort); err != nil { + if err != nil { return err } - f.envExtractor.String(UserEnv, &f.User) - f.envExtractor.Strings(HostsEnv, &f.Hosts) - if _, err := f.envExtractor.Int(PortEnv, &f.Port); err != nil { - return err + if !f.forceNoPrivateKeys { + if privateKeysVal.Present && len(f.PrivateKeysPaths) == 0 { + f.forceNoPrivateKeys = true + } } - f.envExtractor.String(ExtraArgsEnv, &f.ExtraArgs) - - f.envExtractor.String(ConnectionConfigEnv, &f.ConnectionConfigPath) - - f.envExtractor.Bool(LegacyModeEnv, &f.ForceLegacy) - f.envExtractor.Bool(ModernModeEnv, &f.ForceModern) - - f.envExtractor.Bool(AskBastionPasswordEnv, &f.AskBastionPass) - f.envExtractor.Bool(AskSudoPasswordEnv, &f.AskSudoPass) - return nil } @@ -238,7 +235,7 @@ type FlagsParser struct { envsPrefix string ask AskPasswordFunc sett settings.Settings - envsLookup connectionutils.EnvsLookupFunc + envsLookup env.EnvsLookupFunc // extractPrivateKey // custom extract content and password for private key file @@ -273,7 +270,7 @@ func NewFlagsParser(sett settings.Settings) *FlagsParser { // This method trim right all _ ang - symbols and spaces left and right // By default parser add _ after prefix for all env vars func (p *FlagsParser) WithEnvsPrefix(envsPrefix string) *FlagsParser { - p.envsPrefix = connectionutils.SimplifyPrefix(envsPrefix) + p.envsPrefix = env.SimplifyPrefix(envsPrefix) return p } @@ -287,7 +284,7 @@ func (p *FlagsParser) WithAsk(ask AskPasswordFunc) *FlagsParser { return p } -func (p *FlagsParser) WithEnvsLookup(lookup connectionutils.EnvsLookupFunc) *FlagsParser { +func (p *FlagsParser) WithEnvsLookup(lookup env.EnvsLookupFunc) *FlagsParser { if govalue.Nil(lookup) { p.sett.Logger().WarnF("Envs lookup function is nil. Skip set ask function.") return p @@ -604,8 +601,8 @@ func (p *FlagsParser) ParseFlagsAndExtractConfig(arguments []string, set *flag.F return p.ExtractConfigAfterParse(flags, opts...) } -func (p *FlagsParser) envsExtractor() *connectionutils.EnvExtractor { - return connectionutils.NewEnvExtractor(p.envsPrefix, p.envsLookup) +func (p *FlagsParser) envsExtractor() *env.Extractor { + return env.NewExtractor(p.envsPrefix, p.envsLookup) } func (p *FlagsParser) readPrivateKeysFromFlags(flags *Flags, logger log.Logger) ([]AgentPrivateKey, error) { @@ -668,7 +665,7 @@ func (p *FlagsParser) getPasswordsFromUser(flags *Flags) (*passwordsFromUser, er return res, nil } -func getHomeDir(extractor *connectionutils.EnvExtractor) (string, error) { +func getHomeDir(extractor *env.Extractor) (string, error) { home := "" extractor.StringWithoutPrefix("HOME", &home) @@ -707,7 +704,7 @@ func getHomeDir(extractor *connectionutils.EnvExtractor) (string, error) { // returns current user name // first attempt get user from env // can be call multiple times because user.Current() cache user info -func getCurrentUser(extractor *connectionutils.EnvExtractor) (string, error) { +func getCurrentUser(extractor *env.Extractor) (string, error) { userName := "" extractor.StringWithoutPrefix("USER", &userName) diff --git a/pkg/utils/env/extractor.go b/pkg/utils/env/extractor.go index 279c876..8e5950f 100644 --- a/pkg/utils/env/extractor.go +++ b/pkg/utils/env/extractor.go @@ -246,16 +246,22 @@ func (e *Extractor) ExtractAllVars(vars ...*Var) error { continue } - elem.SetInt(int64(destInt)) - val.Present = present + if present { + elem.SetInt(int64(destInt)) + val.Present = present + } case reflect.String: strDest := "" val.Present = e.String(name, &strDest) - elem.SetString(strDest) + if val.Present { + elem.SetString(strDest) + } case reflect.Bool: var destBool bool val.Present = e.Bool(name, &destBool) - elem.SetBool(destBool) + if val.Present { + elem.SetBool(destBool) + } case reflect.Slice: if err := e.processSlice(name, elem, val); err != "" { appendError(name, err) @@ -284,7 +290,9 @@ func (e *Extractor) processSlice(name string, slice reflect.Value, val *Var) str case reflect.String: var destStrSlice []string val.Present = e.Strings(name, &destStrSlice) - slice.Set(reflect.ValueOf(destStrSlice)) + if val.Present { + slice.Set(reflect.ValueOf(destStrSlice)) + } default: return incorrectValErr(kind, true) } diff --git a/pkg/utils/env/extractor_test.go b/pkg/utils/env/extractor_test.go index d0dc047..cb842aa 100644 --- a/pkg/utils/env/extractor_test.go +++ b/pkg/utils/env/extractor_test.go @@ -306,6 +306,31 @@ func TestExtractAll(t *testing.T) { require.Equal(t, -22, dest.Int, "should set val") }) + + t.Run("no rewrite destination is env not present", func(t *testing.T) { + dest := someStruct{ + Bool: true, + String: "my string", + Int: 22, + StringSlice: []string{"first", "second"}, + } + + extractor := getExtractor(make(map[string]string)) + + err := extractor.ExtractAllVars( + NewVar(StringEnv, &dest.String), + NewVar(IntEnv, &dest.Int), + NewVar(BoolEnv, &dest.Bool), + NewVar(SliceEnv, &dest.StringSlice), + ) + + assertErr(err) + + require.Equal(t, "my string", dest.String, "should set rewrite val") + require.Equal(t, 22, dest.Int, "should set rewrite val") + require.True(t, dest.Bool, "should set rewrite val") + require.Equal(t, []string{"first", "second"}, dest.StringSlice, "should set rewrite val") + }) }) } From 32df3cf9eb65ecdb8c905e83b5934760b0332fea Mon Sep 17 00:00:00 2001 From: Nikolay Mitrofanov Date: Sat, 31 Jan 2026 20:09:28 +0300 Subject: [PATCH 5/6] ++ Signed-off-by: Nikolay Mitrofanov --- pkg/ssh/config/parse_flags.go | 67 ++------------------------ pkg/utils/defaults/user.go | 90 +++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 pkg/utils/defaults/user.go diff --git a/pkg/ssh/config/parse_flags.go b/pkg/ssh/config/parse_flags.go index fba948e..36280bf 100644 --- a/pkg/ssh/config/parse_flags.go +++ b/pkg/ssh/config/parse_flags.go @@ -18,7 +18,6 @@ import ( "fmt" "io" "os" - "os/user" "path/filepath" "sort" "strings" @@ -31,6 +30,7 @@ import ( "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/utils" "github.com/deckhouse/lib-connection/pkg/ssh/utils/terminal" + "github.com/deckhouse/lib-connection/pkg/utils/defaults" "github.com/deckhouse/lib-connection/pkg/utils/env" ) @@ -106,7 +106,7 @@ func (f *Flags) IsConflictBetweenFlags() error { func (f *Flags) FillDefaults() error { if len(f.PrivateKeysPaths) == 0 && !f.forceNoPrivateKeys { - home, err := getHomeDir(f.envExtractor) + home, err := defaults.HomeDir(f.envExtractor) if err != nil { return err } @@ -215,7 +215,7 @@ func (f *Flags) userExtractor() func() (string, error) { return *currentUser, nil } - userName, err := getCurrentUser(f.envExtractor) + userName, err := defaults.CurrentUserName(f.envExtractor) if err != nil { return "", err } @@ -665,67 +665,6 @@ func (p *FlagsParser) getPasswordsFromUser(flags *Flags) (*passwordsFromUser, er return res, nil } -func getHomeDir(extractor *env.Extractor) (string, error) { - home := "" - - extractor.StringWithoutPrefix("HOME", &home) - - if home == "" { - var err error - home, err = os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("Cannot get user home dir: %w", err) - } - - if home == "" { - return "", fmt.Errorf("Cannot get user home dir: empty after call os.UserHomeDir") - } - } - - var err error - home, err = filepath.Abs(home) - if err != nil { - return "", fmt.Errorf("Cannot get absolute path of home directory: %w", err) - } - - stat, err := os.Stat(home) - if err != nil { - return "", fmt.Errorf("Cannot get user home dir stat: %w", err) - } - - if !stat.IsDir() { - return "", fmt.Errorf("Cannot get user home dir: '%s' not a directory", home) - } - - return home, nil -} - -// getCurrentUser -// returns current user name -// first attempt get user from env -// can be call multiple times because user.Current() cache user info -func getCurrentUser(extractor *env.Extractor) (string, error) { - userName := "" - - extractor.StringWithoutPrefix("USER", &userName) - - if userName != "" { - return userName, nil - } - - currentUser, err := user.Current() - if err != nil { - return "", fmt.Errorf("cannot get current user: %w", err) - } - - userName = currentUser.Username - if userName == "" { - return "", fmt.Errorf("Cannot get current user: empty after call user.Current") - } - - return userName, nil -} - func fileReader(path string, fileType string) (io.ReadCloser, error) { fullPath, err := filepath.Abs(path) if err != nil { diff --git a/pkg/utils/defaults/user.go b/pkg/utils/defaults/user.go new file mode 100644 index 0000000..a4055d3 --- /dev/null +++ b/pkg/utils/defaults/user.go @@ -0,0 +1,90 @@ +// Copyright 2026 Flant JSC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package defaults + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/deckhouse/lib-connection/pkg/utils/env" +) + +// HomeDir +// extract absolute user home dir in next order +// HOME env +// os.UserHomeDir +// also check that HOME is present and is dir +func HomeDir(extractor *env.Extractor) (string, error) { + home := "" + + extractor.StringWithoutPrefix("HOME", &home) + + if home == "" { + var err error + home, err = os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("Cannot get user home dir: %w", err) + } + + if home == "" { + return "", fmt.Errorf("Cannot get user home dir: empty after call os.UserHomeDir") + } + } + + var err error + home, err = filepath.Abs(home) + if err != nil { + return "", fmt.Errorf("Cannot get absolute path of home directory: %w", err) + } + + stat, err := os.Stat(home) + if err != nil { + return "", fmt.Errorf("Cannot get user home dir stat: %w", err) + } + + if !stat.IsDir() { + return "", fmt.Errorf("Cannot get user home dir: '%s' not a directory", home) + } + + return home, nil +} + +// CurrentUserName +// returns current username +// first attempt get user from env +// can be call multiple times because user.Current() cache user info +func CurrentUserName(extractor *env.Extractor) (string, error) { + userName := "" + + extractor.StringWithoutPrefix("USER", &userName) + + if userName != "" { + return userName, nil + } + + currentUser, err := user.Current() + if err != nil { + return "", fmt.Errorf("cannot get current user: %w", err) + } + + userName = currentUser.Username + if userName == "" { + return "", fmt.Errorf("Cannot get current user: empty after call user.Current") + } + + return userName, nil +} From b79a333123bbdd98bd7cb6ee91d2bc9503e9e696 Mon Sep 17 00:00:00 2001 From: Nikolay Mitrofanov Date: Sat, 31 Jan 2026 20:43:59 +0300 Subject: [PATCH 6/6] ++ Signed-off-by: Nikolay Mitrofanov --- pkg/ssh/config/parse_flags_test.go | 1 + pkg/utils/env/extractor.go | 19 +++++- pkg/utils/env/extractor_test.go | 96 ++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/pkg/ssh/config/parse_flags_test.go b/pkg/ssh/config/parse_flags_test.go index ec94248..e14017e 100644 --- a/pkg/ssh/config/parse_flags_test.go +++ b/pkg/ssh/config/parse_flags_test.go @@ -520,6 +520,7 @@ func TestParseFlags(t *testing.T) { envs: map[string]string{ "DHCTL_SSH_HOSTS": "192.168.0.2,192.168.0.3", "DHCTL_SSH_MODERN_MODE": "true", + "DHCTL_SSH_LEGACY_MODE": "false", "DHCTL_SSH_BASTION_PORT": "2200", }, diff --git a/pkg/utils/env/extractor.go b/pkg/utils/env/extractor.go index 8e5950f..ecc7bfa 100644 --- a/pkg/utils/env/extractor.go +++ b/pkg/utils/env/extractor.go @@ -18,6 +18,7 @@ import ( "fmt" "os" "reflect" + "slices" "strconv" "strings" @@ -146,14 +147,30 @@ func (e *Extractor) Strings(name string, destination *[]string) bool { return true } +var falseBoolValues = []string{ + "false", + "no", + "none", + "0", +} + // Bool +// trim spaces env and to lower value string +// lower value string "false" "no" "none" "0" interpreter as false // returns that env is set func (e *Extractor) Bool(name string, destination *bool) bool { strVar, ok := e.getVar(name) if !ok { return false } - value := strVar != "" + + valueLower := strings.TrimSpace(strings.ToLower(strVar)) + + value := valueLower != "" + + if value && slices.Contains(falseBoolValues, valueLower) { + value = false + } *destination = value diff --git a/pkg/utils/env/extractor_test.go b/pkg/utils/env/extractor_test.go index cb842aa..ad6ec6c 100644 --- a/pkg/utils/env/extractor_test.go +++ b/pkg/utils/env/extractor_test.go @@ -334,6 +334,102 @@ func TestExtractAll(t *testing.T) { }) } +func TestBool(t *testing.T) { + type testCase struct { + name string + envVar string + expected bool + } + + newCase := func(v string, expected bool) testCase { + return testCase{ + name: v, + envVar: v, + expected: expected, + } + } + + falseCase := func(v string) testCase { + return newCase(v, false) + } + + trueCase := func(v string) testCase { + return newCase(v, true) + } + + tests := []testCase{ + { + name: "empty", + envVar: "", + expected: false, + }, + + { + name: "all spaces", + envVar: " ", + expected: false, + }, + + falseCase("NO"), + falseCase("No"), + falseCase("no"), + falseCase("FALSE"), + falseCase("fALSe"), + falseCase("False"), + falseCase("false"), + falseCase("None"), + falseCase("NONE"), + falseCase("none"), + falseCase("0"), + + trueCase("TRUE"), + trueCase("tRUe"), + trueCase("True"), + trueCase("true"), + trueCase("not empty string"), + trueCase("not_empty_string"), + trueCase(" not empty string with spaces "), + } + + const envName = "TST_BOOL" + + getExtractor := func(envs map[string]string) *Extractor { + return NewExtractor("", func(name string) (string, bool) { + value, ok := envs[name] + return value, ok + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envs := map[string]string{ + envName: tt.envVar, + } + + var b bool + + getExtractor(envs).Bool(envName, &b) + + require.Equal(t, tt.expected, b, "should set correct val") + }) + } + + t.Run("empty extract all present set", func(t *testing.T) { + envs := map[string]string{ + envName: "", + } + + var b bool + boolVal := NewVar(envName, &b) + + err := getExtractor(envs).ExtractAllVars(boolVal) + require.NoError(t, err, "should not return error") + + require.False(t, b, "should correct val") + require.True(t, boolVal.Present, "should correct val") + }) +} + func TestSimplifyPrefix(t *testing.T) { require.Equal(t, "MY", SimplifyPrefix(" MY_")) }