diff --git a/Makefile b/Makefile index a850b55..094ca7c 100644 --- a/Makefile +++ b/Makefile @@ -98,8 +98,8 @@ bin/kind: curl-installed bin deps: bin bin/jq bin/golangci-lint bin/gofumpt bin/kind test: go-installed docker-installed bin/kind - # ./hack/run_tests.sh - echo "Skip go tests!!!" + ./hack/run_tests.sh + echo "Run go tests!" $(MAKE) clean/test lint: bin/golangci-lint diff --git a/hack/run_tests.sh b/hack/run_tests.sh index 6949624..8a9e857 100755 --- a/hack/run_tests.sh +++ b/hack/run_tests.sh @@ -18,6 +18,7 @@ source "$(pwd)/hack/utils.sh" check_all_deps check_go +pull_image run_tests="" @@ -80,7 +81,7 @@ function run_tests_in_dir() { echo "Run tests in $full_pkg_path" cd "$full_pkg_path" - if ! echo "test -v -p 1 $run_tests" | xargs go; then + if ! echo "test -timeout 30m -v -p 1 $run_tests" | xargs go; then all_failed_tests="$(echo -e "${all_failed_tests}\nTests in ${p} failed")" fi done <<< "$packages" diff --git a/hack/utils.sh b/hack/utils.sh index 0c9e887..6f495b1 100644 --- a/hack/utils.sh +++ b/hack/utils.sh @@ -98,4 +98,8 @@ function check_kind() { function check_all_deps() { check_docker && check_kind -} \ No newline at end of file +} + +function pull_image() { + docker pull lscr.io/linuxserver/openssh-server:10.0_p1-r9-ls209 +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 4828628..7e3c3a7 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -17,10 +17,11 @@ package provider import ( "context" + "github.com/hashicorp/go-multierror" + connection "github.com/deckhouse/lib-connection/pkg" "github.com/deckhouse/lib-connection/pkg/settings" sshconfig "github.com/deckhouse/lib-connection/pkg/ssh/config" - "github.com/hashicorp/go-multierror" ) type DefaultProvider struct { diff --git a/pkg/provider/ssh.go b/pkg/provider/ssh.go index 675bb03..c46cac8 100644 --- a/pkg/provider/ssh.go +++ b/pkg/provider/ssh.go @@ -332,12 +332,12 @@ func (p *DefaultSSHProvider) newSession(parent *session.Session, privateKeys []s } func (p *DefaultSSHProvider) useGoSSH(shouldLog bool) bool { - logDebug := func(format string, v ...any) { + logDebug := func(format string) { if !shouldLog { return } - p.debug(format, v...) + p.debug(format) } if p.options.ForceGoSSH { diff --git a/pkg/provider/ssh_test.go b/pkg/provider/ssh_test.go index 37b548a..3f6d1f7 100644 --- a/pkg/provider/ssh_test.go +++ b/pkg/provider/ssh_test.go @@ -394,7 +394,7 @@ func TestSSHProviderClient(t *testing.T) { assertSwitchClient(t, params, defaultClient) } - getProvider := func(sett settings.Settings, config *sshconfig.ConnectionConfig, opts ...SSHClientOption) *DefaultSSHProvider { + getProvider := func(sett settings.Settings, config *sshconfig.ConnectionConfig) *DefaultSSHProvider { provider := NewDefaultSSHProvider(sett, config) provider.goSSHStopWait = 3 * time.Second return provider @@ -994,7 +994,7 @@ func assertClientStopped(t *testing.T, client connection.SSHClient, shouldStop b func assertSwitchClient(t *testing.T, params assertSwitchClientParams, defaultClient connection.SSHClient) connection.SSHClient { switchClientSession := defaultSession(params.host, params.port) - var privateKeys []session.AgentPrivateKey + privateKeys := make([]session.AgentPrivateKey, 0, len(params.additionalPrivateKeys)) for _, key := range params.additionalPrivateKeys { privateKeys = append(privateKeys, session.AgentPrivateKey{ Key: key.Key, diff --git a/pkg/ssh/clissh/agent.go b/pkg/ssh/clissh/agent.go index e5672fb..e7d7c0e 100644 --- a/pkg/ssh/clissh/agent.go +++ b/pkg/ssh/clissh/agent.go @@ -103,13 +103,13 @@ func (a *Agent) Start() error { logger := a.sshSettings.Logger() - logger.DebugLn("agent: start ssh-agent") + logger.DebugF("agent: start ssh-agent") err := a.agent.Start() if err != nil { return fmt.Errorf("Start ssh-agent: %v", err) } - logger.DebugLn("agent: run ssh-add for keys") + logger.DebugF("agent: run ssh-add for keys") err = a.AddKeys(a.agentSettings.PrivateKeys) if err != nil { return fmt.Errorf("Agent error: %v", err) @@ -128,7 +128,7 @@ func (a *Agent) AddKeys(keys []session.AgentPrivateKey) error { logger := a.sshSettings.Logger() if a.sshSettings.IsDebug() { - logger.DebugLn("list added keys") + logger.DebugF("list added keys") listCmd := cmd.NewSSHAdd(a.sshSettings, a.agentSettings).ListCmd() output, err := listCmd.CombinedOutput() @@ -163,11 +163,8 @@ func (a *Agent) addKeys(authSock string, keys []session.AgentPrivateKey) error { agentClient := agent.NewClient(conn) - for i, key := range keys { - privateKey, _, err := utils.ParseSSHPrivateKey( - []byte(key.Key), - fmt.Sprintf("index %d", i), - utils.NewDefaultPassphraseOnlyConsumer(key.Passphrase)) + for _, key := range keys { + privateKey, _, err := utils.ParseSSHPrivateKeyFile(key.Key, key.Passphrase, a.sshSettings.Logger()) if err != nil { return err } diff --git a/pkg/ssh/clissh/client.go b/pkg/ssh/clissh/client.go index 3a4f09b..fd77c25 100644 --- a/pkg/ssh/clissh/client.go +++ b/pkg/ssh/clissh/client.go @@ -131,6 +131,10 @@ func (s *Client) Session() *session.Session { return s.SessionSettings } +func (s *Client) Settings() settings.Settings { + return s.settings +} + func (s *Client) PrivateKeys() []session.AgentPrivateKey { return s.privateKeys } diff --git a/pkg/ssh/clissh/cmd/scp.go b/pkg/ssh/clissh/cmd/scp.go index 5ab247f..1ab401c 100644 --- a/pkg/ssh/clissh/cmd/scp.go +++ b/pkg/ssh/clissh/cmd/scp.go @@ -168,7 +168,8 @@ func (s *SCP) SCP(ctx context.Context) *SCP { dstPath, }...) - scpArgs := append(sshPathArgs, args...) + scpArgs := sshPathArgs + scpArgs = append(scpArgs, args...) s.scpCmd = exec.CommandContext(ctx, "scp", scpArgs...) s.scpCmd.Env = env // scpCmd.Stdout = os.Stdout diff --git a/pkg/ssh/clissh/cmd/ssh-add.go b/pkg/ssh/clissh/cmd/ssh-add.go index afdf1b5..75208ac 100644 --- a/pkg/ssh/clissh/cmd/ssh-add.go +++ b/pkg/ssh/clissh/cmd/ssh-add.go @@ -80,7 +80,7 @@ func (s *SSHAdd) AddKeys(keys []string) error { } if s.settings.IsDebug() { - logger.DebugLn("list added keys") + logger.DebugF("List added keys") env := []string{ s.AgentSettings.AuthSockEnv(), } diff --git a/pkg/ssh/clissh/command.go b/pkg/ssh/clissh/command.go index bb2dc4b..ba273dc 100644 --- a/pkg/ssh/clissh/command.go +++ b/pkg/ssh/clissh/command.go @@ -121,12 +121,12 @@ func (c *Command) Sudo(ctx context.Context) { } if !passSent { // send pass through stdin - logger.DebugLn("Send become pass to cmd") + logger.DebugF("Send become pass to cmd") _, _ = c.Executor.Stdin.Write([]byte(becomePass + "\n")) passSent = true } else { // Second prompt is error! - logger.ErrorLn("Bad sudo password") + logger.ErrorF("Bad sudo password") // sending wrong password again will raise an error in process.Run() _, _ = c.Executor.Stdin.Write([]byte(becomePass + "\n")) // os.Exit(1) @@ -134,7 +134,7 @@ func (c *Command) Sudo(ctx context.Context) { return "reset" } if pattern == "SUDO-SUCCESS" { - logger.DebugLn("Got SUCCESS") + logger.DebugF("Got SUCCESS") if c.onCommandStart != nil { c.onCommandStart() } diff --git a/pkg/ssh/clissh/kube-proxy.go b/pkg/ssh/clissh/kube-proxy.go index 38af0a1..0ec3f93 100644 --- a/pkg/ssh/clissh/kube-proxy.go +++ b/pkg/ssh/clissh/kube-proxy.go @@ -56,7 +56,7 @@ func NewKubeProxy(sett settings.Settings, sess *session.Session) *KubeProxy { } } -func (k *KubeProxy) Start(useLocalPort int) (port string, err error) { +func (k *KubeProxy) Start(useLocalPort int) (string, error) { startID := rand.Int() logger := k.settings.Logger() @@ -114,13 +114,12 @@ func (k *KubeProxy) StopAll() { } func (k *KubeProxy) Stop(startID int) { - logger := k.settings.Logger() - if k == nil { - logger.DebugF("[%d] Stop kube-proxy: kube proxy object is nil. Skip.\n", startID) return } + logger := k.settings.Logger() + if k.stop { logger.DebugF("[%d] Stop kube-proxy: kube proxy already stopped. Skip.\n", startID) return @@ -244,7 +243,7 @@ func (k *KubeProxy) upTunnel( useLocalPort int, tunnelErrorCh chan error, startID int, -) (tun *Tunnel, localPort int, err error) { +) (*Tunnel, int, error) { logger := k.settings.Logger() logger.DebugF( @@ -255,7 +254,7 @@ func (k *KubeProxy) upTunnel( ) rewriteLocalPort := false - localPort = useLocalPort + localPort := useLocalPort if useLocalPort < 1 { logger.DebugF( @@ -271,6 +270,7 @@ func (k *KubeProxy) upTunnel( maxRetries := 5 retries := 0 var lastError error + var tun *Tunnel for { logger.DebugF("[%d] Start %d iteration for up tunnel\n", startID, retries) @@ -327,13 +327,13 @@ func (k *KubeProxy) upTunnel( func (k *KubeProxy) runKubeProxy( waitCh chan error, startID int, -) (proxy *Command, port string, err error) { +) (*Command, string, error) { logger := k.settings.Logger() logger.DebugF("[%d] Begin starting proxy\n", startID) - proxy = k.proxyCMD(startID) + proxy := k.proxyCMD(startID) - port = "" + port := "" portReady := make(chan struct{}, 1) portRe := regexp.MustCompile(`Starting to serve on .*?:(\d+)`) @@ -358,7 +358,7 @@ func (k *KubeProxy) runKubeProxy( }) logger.DebugF("[%d] Start proxy command\n", startID) - err = proxy.Start() + err := proxy.Start() if err != nil { logger.DebugF("[%d] Start proxy command error: %v\n", startID, err) return nil, "", fmt.Errorf("start kubectl proxy: %w", err) diff --git a/pkg/ssh/clissh/process/executor.go b/pkg/ssh/clissh/process/executor.go index 9407cd1..c9dbb86 100644 --- a/pkg/ssh/clissh/process/executor.go +++ b/pkg/ssh/clissh/process/executor.go @@ -230,7 +230,7 @@ func (e *Executor) StderrBytes() []byte { return nil } -func (e *Executor) SetupStreamHandlers() (err error) { +func (e *Executor) SetupStreamHandlers() error { // stderr goes to console (commented because ssh writes only "Connection closed" messages to stderr) // e.Cmd.Stderr = os.Stderr // connect console's stdin @@ -239,9 +239,10 @@ func (e *Executor) SetupStreamHandlers() (err error) { // setup stdout stream handlers if e.Live && e.StdoutBuffer == nil && e.StdoutHandler == nil && len(e.Matchers) == 0 { e.cmd.Stdout = os.Stdout - return + return nil } + var err error var stdoutReadPipe *os.File var stdoutHandlerWritePipe *os.File var stdoutHandlerReadPipe *os.File @@ -328,8 +329,8 @@ func (e *Executor) SetupStreamHandlers() (err error) { return } - logger.DebugLn("Start reading from stderr pipe") - defer logger.DebugLn("Stop reading from stderr pipe") + logger.DebugF("Start reading from stderr pipe") + defer logger.DebugF("Stop reading from stderr pipe") buf := make([]byte, 16) for { @@ -366,7 +367,7 @@ func (e *Executor) SetupStreamHandlers() (err error) { func (e *Executor) readFromStreams(stdoutReadPipe io.Reader, stdoutHandlerWritePipe io.Writer) { logger := e.settings.Logger() - defer logger.DebugLn("stop readFromStreams") + defer logger.DebugF("stop readFromStreams") if stdoutReadPipe == nil || reflect.ValueOf(stdoutReadPipe).IsNil() { return @@ -375,7 +376,7 @@ func (e *Executor) readFromStreams(stdoutReadPipe io.Reader, stdoutHandlerWriteP logger.DebugF("Start read from streams for command: ", e.cmd.String()) buf := make([]byte, 16) - matchersDone := false + var matchersDone bool if len(e.Matchers) == 0 { matchersDone = true } @@ -429,7 +430,7 @@ func (e *Executor) readFromStreams(stdoutReadPipe io.Reader, stdoutHandlerWriteP } if err == io.EOF { - logger.DebugLn("readFromStreams: EOF") + logger.DebugF("readFromStreams: EOF") break } } @@ -536,8 +537,8 @@ func (e *Executor) ProcessWait() { func (e *Executor) closePipes() { logger := e.settings.Logger() - logger.DebugLn("Starting close piped") - defer logger.DebugLn("Stop close piped") + logger.DebugF("Starting close piped") + defer logger.DebugF("Stop close piped") e.pipesMutex.Lock() defer e.pipesMutex.Unlock() diff --git a/pkg/ssh/clissh/process/session.go b/pkg/ssh/clissh/process/session.go index 61231ce..56761b5 100644 --- a/pkg/ssh/clissh/process/session.go +++ b/pkg/ssh/clissh/process/session.go @@ -55,7 +55,7 @@ func (s *Session) Stop() { s.Stop() }(stopable) } - //log.DebugF("Wait while %d processes stops\n", count) + // log.DebugF("Wait while %d processes stops\n", count) wg.Wait() } diff --git a/pkg/ssh/clissh/reverse-tunnel.go b/pkg/ssh/clissh/reverse-tunnel.go index 0434ae1..72ed2b5 100644 --- a/pkg/ssh/clissh/reverse-tunnel.go +++ b/pkg/ssh/clissh/reverse-tunnel.go @@ -23,11 +23,12 @@ import ( "sync" "time" + "github.com/deckhouse/lib-dhctl/pkg/retry" + connection "github.com/deckhouse/lib-connection/pkg" "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/clissh/cmd" "github.com/deckhouse/lib-connection/pkg/ssh/session" - "github.com/deckhouse/lib-dhctl/pkg/retry" ) type tunnelWaitResult struct { @@ -146,7 +147,6 @@ func (t *ReverseTunnel) StartHealthMonitor(ctx context.Context, checker connecti logger := t.settings.Logger() checkReverseTunnel := func(id int) bool { - logger.DebugF("[%d] Start Check reverse tunnel\n", id) err := retry.NewSilentLoop("Check reverse tunnel", 2, 2*time.Second).RunContext(ctx, func() error { @@ -169,7 +169,7 @@ func (t *ReverseTunnel) StartHealthMonitor(ctx context.Context, checker connecti } go func() { - logger.DebugLn("Start health monitor") + logger.DebugF("Start health monitor") // we need chan for restarting because between restarting we can get stop signal restartCh := make(chan int, 1024) id := -1 @@ -180,14 +180,13 @@ func (t *ReverseTunnel) StartHealthMonitor(ctx context.Context, checker connecti logger.DebugF("[%d] Signal was sent. Chan len: %d\n", id, len(restartCh)) } for { - if !checkReverseTunnel(id) { go restart(id) } select { case <-t.stopCh: - logger.DebugLn("Stop health monitor") + logger.DebugF("Stop health monitor") return case oldId := <-restartCh: restartsCount++ diff --git a/pkg/ssh/clissh/upload-script.go b/pkg/ssh/clissh/upload-script.go index d5ecc7c..17d169e 100644 --- a/pkg/ssh/clissh/upload-script.go +++ b/pkg/ssh/clissh/upload-script.go @@ -110,11 +110,11 @@ func (u *UploadScript) Settings() settings.Settings { return u.settings } -func (u *UploadScript) Execute(ctx context.Context) (stdout []byte, err error) { +func (u *UploadScript) Execute(ctx context.Context) ([]byte, error) { scriptName := filepath.Base(u.ScriptPath) remotePath := genssh.ExecuteRemoteScriptPath(u, scriptName, false) - err = NewFile(u.settings, u.Session).Upload(ctx, u.ScriptPath, remotePath) + err := NewFile(u.settings, u.Session).Upload(ctx, u.ScriptPath, remotePath) if err != nil { return nil, fmt.Errorf("upload: %v", err) } @@ -182,12 +182,12 @@ func (u *UploadScript) pathWithEnv(path string) string { var ErrBashibleTimeout = errors.New("Timeout bashible step running") -func (u *UploadScript) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) (stdout []byte, err error) { +func (u *UploadScript) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) ([]byte, error) { bundleName := fmt.Sprintf("bundle-%s.tar", time.Now().Format("20060102-150405")) bundleLocalFilepath := filepath.Join(u.settings.TmpDir(), bundleName) // tar cpf bundle.tar -C /tmp/dhctl.1231qd23/var/lib bashible - err = tar.CreateTar(bundleLocalFilepath, parentDir, bundleDir) + err := tar.CreateTar(bundleLocalFilepath, parentDir, bundleDir) if err != nil { return nil, fmt.Errorf("tar bundle: %v", err) } @@ -198,7 +198,7 @@ func (u *UploadScript) ExecuteBundle(ctx context.Context, parentDir, bundleDir s ) // upload to node's deckhouse tmp directory - err = NewFile(u.settings, u.Session).Upload(ctx, bundleLocalFilepath, u.settings.TmpDir()) + err = NewFile(u.settings, u.Session).Upload(ctx, bundleLocalFilepath, u.settings.NodeTmpDir()) if err != nil { return nil, fmt.Errorf("upload: %v", err) } @@ -207,7 +207,7 @@ func (u *UploadScript) ExecuteBundle(ctx context.Context, parentDir, bundleDir s // tar xpof ${app.DeckhouseNodeTmpPath}/bundle.tar -C /var/lib && /var/lib/bashible/bashible.sh args... tarCmdline := fmt.Sprintf( "tar xpof %s/%s -C /var/lib && /var/lib/%s/%s %s", - u.settings.TmpDir(), + u.settings.NodeTmpDir(), bundleName, bundleDir, u.ScriptPath, @@ -318,6 +318,6 @@ func bundleOutputHandler( } stepLogs = append(stepLogs, l) - logger.DebugLn(l) + logger.DebugF(l) } } diff --git a/pkg/ssh/config/common_test.go b/pkg/ssh/config/common_test.go index a75ecea..aed17a0 100644 --- a/pkg/ssh/config/common_test.go +++ b/pkg/ssh/config/common_test.go @@ -1,3 +1,17 @@ +// 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 config import ( @@ -131,9 +145,9 @@ host: "{{ . }}" func generateConfigWithKeys(t *testing.T, keys []AgentPrivateKey, additionalFields string, hosts ...string) string { var keysMap []map[string]string - keysJson, err := json.Marshal(keys) + keysJSON, err := json.Marshal(keys) require.NoError(t, err) - err = json.Unmarshal(keysJson, &keysMap) + err = json.Unmarshal(keysJSON, &keysMap) require.NoError(t, err) if additionalFields == "" { @@ -162,7 +176,7 @@ func init() { testConfigTemplateEngine, err = template.New("test_connection_config").Funcs(template.FuncMap{ "indent": func(spaces int, v string) string { pad := strings.Repeat(" ", spaces) - return pad + strings.Replace(v, "\n", "\n"+pad, -1) + return pad + strings.ReplaceAll(v, "\n", "\n"+pad) }, }).Parse(testConfigTemplate) diff --git a/pkg/ssh/config/parse_flags.go b/pkg/ssh/config/parse_flags.go index 81c6440..837fa66 100644 --- a/pkg/ssh/config/parse_flags.go +++ b/pkg/ssh/config/parse_flags.go @@ -858,16 +858,7 @@ func (e *envExtractor) Bool(name string, destination *bool) bool { } func terminalPrivateKeyPasswordExtractor(path string, defaultPassword []byte, logger log.Logger) (string, error) { - content, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("Cannot read private key %s: %w", path, err) - } - - _, password, err := utils.ParseSSHPrivateKey( - content, - path, - utils.NewTerminalPassphraseConsumer(logger, defaultPassword), - ) + _, password, err := utils.ParseSSHPrivateKeyFile(path, string(defaultPassword), logger) return password, err } @@ -885,7 +876,8 @@ func intPtr(i int) *int { } func (h notUniqueHosts) flagsError() error { - var errs []string + errs := make([]string, 0) + if h.noHosts { errs = append(errs, fmt.Sprintf("SSH hosts for connection is required. Please pass hosts for connection via --%s flag", sshHostsFlag)) } diff --git a/pkg/ssh/config/parse_flags_test.go b/pkg/ssh/config/parse_flags_test.go index 7f81bf5..01dcb98 100644 --- a/pkg/ssh/config/parse_flags_test.go +++ b/pkg/ssh/config/parse_flags_test.go @@ -201,7 +201,7 @@ func TestParseFlags(t *testing.T) { // by default, we have ~/.ssh/id_rsa key // it can be protected with password with local development env defaultPrivateKeyExtractor := func(homePath string) PrivateKeyExtractorFunc { - return func(path string, logger log.Logger) (password string, err error) { + return func(path string, logger log.Logger) (string, error) { expected := filepath.Join(homePath, ".ssh", "id_rsa") if path != expected { return "", fmt.Errorf("expected %s, got %s", homePath, path) @@ -230,7 +230,7 @@ func TestParseFlags(t *testing.T) { defaultAsk bool } - beforeAddPrivateKeys := func(t *testing.T, tst *test, logger log.Logger) { + beforeAddPrivateKeys := func(_ *testing.T, tst *test, _ log.Logger) { pathToPassword := make(map[string]string) for _, privateKey := range tst.privateKeys { @@ -1032,10 +1032,10 @@ sshBastionPassword: "not_secure_password_bastion" return nil, fmt.Errorf("no passwords set") } - switch true { - case promt == "[bastion] Password: ": + switch promt { + case "[bastion] Password: ": return []byte(tst.passwords.Bastion), nil - case promt == "[sudo] Password: ": + case "[sudo] Password: ": return []byte(tst.passwords.Sudo), nil default: return nil, fmt.Errorf("unknown prompt") @@ -1252,7 +1252,8 @@ func TestParseFlagsAndExtractConfigNoArgs(t *testing.T) { params.arguments[0], } - os.Args = append(withAdditional, testArgs...) + withAdditional = append(withAdditional, testArgs...) + os.Args = withAdditional // should extract from os.Args params.arguments = nil diff --git a/pkg/ssh/gossh/client.go b/pkg/ssh/gossh/client.go index c02331a..dd2b62f 100644 --- a/pkg/ssh/gossh/client.go +++ b/pkg/ssh/gossh/client.go @@ -165,6 +165,10 @@ func (s *Client) Session() *session.Session { return s.sessionClient } +func (s *Client) Settings() settings.Settings { + return s.settings +} + func (s *Client) PrivateKeys() []session.AgentPrivateKey { return s.privateKeys } @@ -671,17 +675,13 @@ func (s *Client) dialContext(ctx context.Context, network, addr string, config * func (s *Client) initSigners() error { if len(s.signers) > 0 { - s.settings.Logger().DebugLn("Signers already initialized") + s.settings.Logger().DebugF("Signers already initialized") return nil } signers := make([]gossh.Signer, 0, len(s.privateKeys)) - for i, keypath := range s.privateKeys { - key, _, err := utils.ParseSSHPrivateKey( - []byte(keypath.Key), - fmt.Sprintf("index %d", i), - utils.NewDefaultPassphraseOnlyConsumer(keypath.Passphrase), - ) + for _, keypath := range s.privateKeys { + key, _, err := utils.ParseSSHPrivateKeyFile(keypath.Key, keypath.Passphrase, s.settings.Logger()) if err != nil { return err } @@ -754,7 +754,7 @@ func (s *Client) stopAll(cause string) []error { } if err := sess.Close(); err != nil { - addError(err, "Failed to close session %d: %v", indx) + addError(err, "Failed to close session %d: %v", indx, err) } } s.sshSessionsList = nil diff --git a/pkg/ssh/gossh/client_test.go b/pkg/ssh/gossh/client_test.go index 1052c3e..97e0e6b 100644 --- a/pkg/ssh/gossh/client_test.go +++ b/pkg/ssh/gossh/client_test.go @@ -104,7 +104,6 @@ func TestOnlyPreparePrivateKeys(t *testing.T) { assertError(t, c, err) }) } - }) } @@ -275,7 +274,7 @@ func TestClientStart(t *testing.T) { if !c.wantErr { require.NoError(t, err) - test.Logger.InfoLn("client started successfully") + test.Logger.DebugF("client started successfully") return } @@ -321,7 +320,7 @@ func TestClientKeepalive(t *testing.T) { defer func() { err := s.Close() if err != nil { - test.Logger.InfoF("failed to close runEcho session: %v", err) + test.Logger.DebugF("failed to close runEcho session: %v", err) } }() @@ -333,6 +332,8 @@ func TestClientKeepalive(t *testing.T) { runEcho(t, "Hello before restart") err = container.Container.Restart(true, 2*time.Second) + // we must wait for keepalive will restart the client. By default, it takes at least 15s, so we double it to be sure it's restarted + time.Sleep(30 * time.Second) require.NoError(t, err, "failed to restart container") waitKeepAlive() diff --git a/pkg/ssh/gossh/command.go b/pkg/ssh/gossh/command.go index 699a48a..4549fa5 100644 --- a/pkg/ssh/gossh/command.go +++ b/pkg/ssh/gossh/command.go @@ -207,16 +207,16 @@ func (c *SSHCommand) wait() error { select { case err := <-c.ctxResult: - if c.ctxResult != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil - } - } + // if c.ctxResult != nil { + // if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + // return nil + // } + // } + return err case err := <-waitCh: if err != nil { return err } - } return nil } @@ -402,7 +402,7 @@ func (c *SSHCommand) Sudo(ctx context.Context) { passSent = true } else { // Second prompt is error! - logger.ErrorLn("Bad sudo password.") + logger.ErrorF("Bad sudo password.") } return "reset" } @@ -546,7 +546,7 @@ func (c *SSHCommand) CaptureStderr(buf *bytes.Buffer) *SSHCommand { return c } -func (c *SSHCommand) SetupStreamHandlers() (err error) { +func (c *SSHCommand) SetupStreamHandlers() error { // stderr goes to console (commented because ssh writes only "Connection closed" messages to stderr) // c.Cmd.Stderr = os.Stderr // connect console's stdin @@ -557,13 +557,13 @@ func (c *SSHCommand) SetupStreamHandlers() (err error) { c.session.Stdout = os.Stdout c.session.Stdout = &c.OutBytes c.session.Stderr = &c.ErrBytes - return + return nil } + var err error var stdoutHandlerWritePipe *os.File var stdoutHandlerReadPipe *os.File if c.out != nil || c.stdoutHandler != nil || len(c.Matchers) > 0 { - if c.out == nil { c.out = new(bytes.Buffer) } @@ -589,7 +589,6 @@ func (c *SSHCommand) SetupStreamHandlers() (err error) { var stderrHandlerWritePipe *os.File var stderrHandlerReadPipe *os.File if c.err != nil || c.stderrHandler != nil || len(c.Matchers) > 0 { - if c.err == nil { c.err = new(bytes.Buffer) } @@ -628,7 +627,7 @@ func (c *SSHCommand) SetupStreamHandlers() (err error) { c.readFromStreams(c.stdoutPipeFile, stdoutHandlerWritePipe, false) }() - // sudo hack, becouse of password prompt is sent to STDERR, not STDOUT + // sudo hack, because of password prompt is sent to STDERR, not STDOUT go func() { c.readFromStreams(c.stderrPipeFile, stdoutHandlerWritePipe, true) }() @@ -740,18 +739,18 @@ func (c *SSHCommand) readFromStreams(stdoutReadPipe io.Reader, stdoutHandlerWrit } // TODO logboek if c.sshClient.settings.IsDebug() { - os.Stdout.Write(buf[m:n]) + _, _ = os.Stdout.Write(buf[m:n]) } if c.out != nil && !isError { - c.out.Write(buf[:n]) + _, _ = c.out.Write(buf[:n]) } if c.err != nil && isError { - c.err.Write(buf[:n]) + _, _ = c.err.Write(buf[:n]) } if c.combined != nil { - c.combined.Write(buf[:n]) + _, _ = c.combined.Write(buf[:n]) } if c.stdoutHandler != nil { _, _ = stdoutHandlerWritePipe.Write(buf[m:n]) @@ -805,9 +804,9 @@ func (c *SSHCommand) Stop() { } c.logDebugF("Stopped") c.logDebugF("Sending SIGINT...") - c.session.Signal(gossh.SIGINT) + _ = c.session.Signal(gossh.SIGINT) c.logDebugF("Signal SIGINT sent") - c.session.Signal(gossh.SIGKILL) + _ = c.session.Signal(gossh.SIGKILL) } func (c *SSHCommand) setWaitError(err error) { diff --git a/pkg/ssh/gossh/command_test.go b/pkg/ssh/gossh/command_test.go index 9c3bf06..fe3e7f3 100644 --- a/pkg/ssh/gossh/command_test.go +++ b/pkg/ssh/gossh/command_test.go @@ -53,12 +53,12 @@ func TestCommandOutput(t *testing.T) { wantErr: false, }, { - title: "With context", - command: `while true; do echo "test"; sleep 5; done`, - args: []string{}, - expectedOutput: "test\ntest\n", - timeout: 7 * time.Second, - wantErr: false, + title: "With context", + command: `while true; do echo "test"; sleep 5; done`, + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", }, { title: "Command return error", @@ -183,12 +183,13 @@ func TestCommandCombinedOutput(t *testing.T) { wantErr: false, }, { - title: "With context", - command: "while true; do echo \"test\"; sleep 5; done", - args: []string{}, - expectedOutput: "test\ntest\n", - timeout: 7 * time.Second, - wantErr: false, + title: "With context", + command: "while true; do echo \"test\"; sleep 5; done", + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", + expectedErrOutput: "test\ntest\n", }, { title: "Command return error", @@ -312,25 +313,26 @@ func TestCommandRun(t *testing.T) { { title: "Just echo, success", command: "echo", - args: []string{`"est output"`}, + args: []string{"\"test output\""}, expectedOutput: "test output\n", wantErr: false, }, { title: "Just echo, with envs, success", command: "echo", - args: []string{`test output"`}, + args: []string{"\"test output\""}, expectedOutput: "test output\n", envs: envs, wantErr: false, }, { - title: "With context", - command: `while true; do echo "test"; sleep 5; done`, - args: []string{}, - expectedOutput: "test\ntest\n", - timeout: 7 * time.Second, - wantErr: false, + title: "With context", + command: "while true; do echo \"test\"; sleep 5; done", + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", + expectedErrOutput: "test\ntest\n", }, { title: "Command return error", @@ -343,7 +345,7 @@ func TestCommandRun(t *testing.T) { { title: "With opened stdout pipe", command: "echo", - args: []string{`"test output\"`}, + args: []string{"\"test output\""}, prepareFunc: func(c *SSHCommand) error { return c.Run(context.Background()) }, @@ -353,7 +355,7 @@ func TestCommandRun(t *testing.T) { { title: "With nil session", command: "echo", - args: []string{`"test output"`}, + args: []string{"\"test output\""}, prepareFunc: func(c *SSHCommand) error { err := c.session.Close() c.session = nil @@ -415,7 +417,6 @@ func TestCommandRun(t *testing.T) { // command should fail to run require.Error(t, err) require.Contains(t, err.Error(), "context deadline exceeded") - } sshClient.Stop() }) @@ -507,7 +508,7 @@ func TestCommandStart(t *testing.T) { test.Logger.ErrorF("SSH-agent process exited, now stop. Wait error: %v", err) return } - test.Logger.InfoF("SSH-agent process exited, now stop") + test.Logger.DebugF("SSH-agent process exited, now stop") }) return nil }, @@ -552,7 +553,9 @@ func TestCommandSudoRun(t *testing.T) { t, test, sshtesting.WithPassword(sshtesting.RandPassword(12)), + sshtesting.WithConnectToContainerNetwork(container), ) + keysContainerWithPass := containerWithPass.AgentPrivateKeys() sessionWithoutPassword := sshtesting.Session(container) @@ -595,7 +598,7 @@ func TestCommandSudoRun(t *testing.T) { { title: "Just echo, failure, with wrong password", settings: sessionWithInvalidPass, - keys: keys, + keys: keysContainerWithPass, command: "echo", args: []string{`"test output"`}, wantErr: true, @@ -609,7 +612,8 @@ func TestCommandSudoRun(t *testing.T) { command: `while true; do echo "test"; sleep 5; done`, args: []string{}, timeout: 7 * time.Second, - wantErr: false, + wantErr: true, + err: "context deadline exceeded", }, } diff --git a/pkg/ssh/gossh/common_test.go b/pkg/ssh/gossh/common_test.go index d6698cd..1a2d299 100644 --- a/pkg/ssh/gossh/common_test.go +++ b/pkg/ssh/gossh/common_test.go @@ -1,15 +1,32 @@ +// 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 gossh import ( "context" "fmt" "net" + "regexp" + "strings" "testing" "time" - sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" "github.com/deckhouse/lib-dhctl/pkg/retry" "github.com/stretchr/testify/require" + + sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" ) func registerStopClient(t *testing.T, sshClient *Client) { @@ -48,8 +65,8 @@ func startContainerAndClientWithContainer(t *testing.T, test *sshtesting.Test, o return sshClient, container } -func startContainerAndClient(t *testing.T, test *sshtesting.Test, opts ...sshtesting.TestContainerWrapperSettingsOpts) *Client { - sshClient, _ := startContainerAndClientWithContainer(t, test, opts...) +func startContainerAndClient(t *testing.T, test *sshtesting.Test) *Client { + sshClient, _ := startContainerAndClientWithContainer(t, test) return sshClient } @@ -73,3 +90,51 @@ func registerStopTunnel(t *testing.T, tunnel *Tunnel) { tunnel.Stop() }) } + +func startContainerAndClientAndKind(t *testing.T, test *sshtesting.Test, opts ...sshtesting.TestContainerWrapperSettingsOpts) (*Client, *sshtesting.TestContainerWrapper) { + sshClient, container := startContainerAndClientWithContainer(t, test, opts...) + + err := sshtesting.CreateKINDCluster() + require.NoError(t, err) + + t.Cleanup(func() { + _ = sshtesting.DeleteKindCluster() + }) + + err = container.Container.DockerNetworkConnect(false, "kind") + require.NoError(t, err) + + ip, err := sshtesting.GetKINDControlPlaneIP() + require.NoError(t, err) + ip = strings.TrimSpace(ip) + + kubeconfig, err := sshtesting.GetKINDKubeconfig() + require.NoError(t, err) + + re := regexp.MustCompile("127[.]0[.]0[.]1:[0-9]{4,5}") + newKubeconfig := re.ReplaceAllString(kubeconfig, ip+":6443") + + err = container.Container.CreateDirectory("/config/.kube") + require.NoError(t, err) + + // TODO revome it. w/o sleep file upload failed + time.Sleep(30 * time.Second) + + config := test.MustCreateTmpFile(t, newKubeconfig, false, "config") + file := sshClient.File() + err = retry.NewLoop("uploading kubeconfig", 20, 3*time.Second).Run(func() error { + return file.Upload(context.Background(), config, ".kube/config") + }) + + require.NoError(t, err) + + err = container.Container.DownloadKubectl("v1.35.0") + require.NoError(t, err) + + err = container.Container.CreateDirectory("/etc/kubernetes/") + require.NoError(t, err) + err = container.Container.ExecToContainer("symlink of kubeconfig", "ln", "-s", "/config/.kube/config", "/etc/kubernetes/admin.conf") + require.NoError(t, err) + + return sshClient, container +} diff --git a/pkg/ssh/gossh/file.go b/pkg/ssh/gossh/file.go index 2fd20be..2bed1e6 100644 --- a/pkg/ssh/gossh/file.go +++ b/pkg/ssh/gossh/file.go @@ -27,10 +27,11 @@ import ( "sync" "github.com/bramvdbogaerde/go-scp" - "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-dhctl/pkg/log" gossh "github.com/deckhouse/lib-gossh" "github.com/google/uuid" + + "github.com/deckhouse/lib-connection/pkg/settings" ) type SSHFile struct { @@ -168,7 +169,10 @@ func (f *SSHFile) Download(ctx context.Context, remotePath, dstPath string) erro re := regexp.MustCompile(`\s+`) files := re.Split(filesString, -1) for _, file := range files { - f.Download(ctx, remotePath+"/"+file, dstPath+"/"+file) + err = f.Download(ctx, remotePath+"/"+file, dstPath+"/"+file) + if err != nil { + return err + } } } @@ -384,7 +388,6 @@ func checkResponse(r io.Reader) error { } return nil - } func wait(wg *sync.WaitGroup, ctx context.Context) error { @@ -404,7 +407,6 @@ func wait(wg *sync.WaitGroup, ctx context.Context) error { } func CopyFromRemote(ctx context.Context, file *os.File, remotePath string, sshClient *gossh.Client) error { - session, err := sshClient.NewSession() if err != nil { return fmt.Errorf("Error creating ssh session in copy from remote: %v", err) @@ -423,7 +425,6 @@ func CopyFromRemote(ctx context.Context, file *os.File, remotePath string, sshCl errCh <- err // We must unblock the go routine first as we block on reading the channel later wg.Done() - }() r, err := session.StdoutPipe() diff --git a/pkg/ssh/gossh/file_test.go b/pkg/ssh/gossh/file_test.go index 8f89378..ec7ee7e 100644 --- a/pkg/ssh/gossh/file_test.go +++ b/pkg/ssh/gossh/file_test.go @@ -23,9 +23,9 @@ import ( "path/filepath" "testing" - "github.com/deckhouse/lib-connection/pkg/settings" "github.com/stretchr/testify/require" + "github.com/deckhouse/lib-connection/pkg/settings" sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" ) @@ -51,6 +51,10 @@ func TestSSHFileUpload(t *testing.T) { err := os.Symlink(testFile, symlink) require.NoError(t, err) + const unaccessibleDirectoryName = "unaccessible" + test.MustCreateUnaccessibleDir(t, unaccessibleDirectoryName) + unaccessibleDirectoryPath := filepath.Join(test.TmpDir(), unaccessibleDirectoryName) + sshClient := startContainerAndClient(t, test) t.Run("Upload files and directories to container via existing ssh client", func(t *testing.T) { @@ -120,7 +124,7 @@ func TestSSHFileUpload(t *testing.T) { }, { title: "Unaccessible dir", - srcPath: "/var/audit", + srcPath: unaccessibleDirectoryPath, dstPath: ".", wantErr: true, err: "could not read directory", @@ -174,6 +178,8 @@ func TestSSHFileUploadBytes(t *testing.T) { test := sshtesting.ShouldNewTest(t, "TestSSHFileUploadBytes") sshClient := startContainerAndClient(t, test) + err := os.MkdirAll(sshClient.settings.TmpDir(), 0o777) + require.NoError(t, err) t.Run("Upload bytes", func(t *testing.T) { const content = "Hello world" @@ -358,11 +364,11 @@ func TestSSHFileDownload(t *testing.T) { err := f.Download(context.Background(), "/tmp/testdata/first", dstPath) // /tmp/testdata/first contains "Some test data" string require.NoError(t, err) - - assertFilesViaRemoteRun(t, sshClient, "cat /tmp/testdata/first", dstPath) - downloadedContent, err := os.ReadFile(dstPath) require.NoError(t, err) + + assertFilesViaRemoteRun(t, sshClient, "cat /tmp/testdata/first", string(downloadedContent)) + // out contains a contant of uploaded file, should be equal to testFile contant require.Equal(t, expectedFileContent, string(downloadedContent)) }) @@ -374,11 +380,11 @@ func TestSSHFileDownload(t *testing.T) { err = f.Download(context.Background(), "/tmp/testdata", downloadWholeDirDir) require.NoError(t, err) - cmd := exec.Command("ls -R", downloadWholeDirDir) + cmd := exec.Command("ls", filepath.Join(downloadWholeDirDir, "testdata")) lsResult, err := cmd.Output() require.NoError(t, err) - assertFilesViaRemoteRun(t, sshClient, "ls -R /tmp/testdata", string(lsResult)) + assertFilesViaRemoteRun(t, sshClient, "ls /tmp/testdata/", string(lsResult)) }) } @@ -397,27 +403,17 @@ func TestSSHFileDownloadBytes(t *testing.T) { cases := []struct { title string remotePath string - tmpDirName string wantErr bool err string }{ { title: "Positive result", remotePath: "/tmp/testfile", - tmpDirName: os.TempDir(), wantErr: false, }, - { - title: "Unaccessible tmp", - remotePath: "/tmp/testfile", - tmpDirName: "/var/lib", - wantErr: true, - err: "create target tmp file", - }, { title: "Unaccessible remote file", remotePath: "/etc/sudoers", - tmpDirName: os.TempDir(), wantErr: true, err: "download target tmp file", }, @@ -430,11 +426,11 @@ func TestSSHFileDownloadBytes(t *testing.T) { if c.wantErr { require.Error(t, err) require.Contains(t, err.Error(), c.err) + } else { + require.NoError(t, err) + // out contains a contant of uploaded file, should be equal to testFile contant + require.Equal(t, expectedFileContent, string(bytes)) } - - require.NoError(t, err) - // out contains a contant of uploaded file, should be equal to testFile contant - require.Equal(t, expectedFileContent, string(bytes)) }) } }) diff --git a/pkg/ssh/gossh/keepalive.go b/pkg/ssh/gossh/keepalive.go index 73399c7..7151050 100644 --- a/pkg/ssh/gossh/keepalive.go +++ b/pkg/ssh/gossh/keepalive.go @@ -121,5 +121,5 @@ func (c *keepAliveChecker) handleClientAliveFailed(err error) error { func (c *keepAliveChecker) debug(format string, a ...any) { debugPrefix := fmt.Sprintf("Keepalive[%d] to %s ", c.id, c.client.sessionClient.String()) format = debugPrefix + format - c.client.settings.Logger().InfoF(format, a...) + c.client.settings.Logger().DebugF(format, a...) } diff --git a/pkg/ssh/gossh/kube-proxy.go b/pkg/ssh/gossh/kube-proxy.go index 2fb8cbe..1bf742a 100644 --- a/pkg/ssh/gossh/kube-proxy.go +++ b/pkg/ssh/gossh/kube-proxy.go @@ -55,7 +55,7 @@ func NewKubeProxy(client *Client, sess *session.Session) *KubeProxy { } } -func (k *KubeProxy) Start(useLocalPort int) (port string, err error) { +func (k *KubeProxy) Start(useLocalPort int) (string, error) { startID := rand.Int() logger := k.sshClient.settings.Logger() @@ -74,6 +74,8 @@ func (k *KubeProxy) Start(useLocalPort int) (port string, err error) { proxyCommandErrorCh := make(chan error, 1) var proxy *SSHCommand + var port string + var err error for { proxy, port, err = k.runKubeProxy(proxyCommandErrorCh, startID) if err != nil { @@ -89,7 +91,7 @@ func (k *KubeProxy) Start(useLocalPort int) (port string, err error) { if portNum > 1024 { break } - logger.DebugF("Proxy run on priveleged port %s and will be stopped and restarted\n", port) + logger.DebugF("Proxy run on privileged port %s and will be stopped and restarted\n", port) k.Stop(startID) } @@ -128,13 +130,12 @@ func (k *KubeProxy) StopAll() { } func (k *KubeProxy) Stop(startID int) { - logger := k.sshClient.settings.Logger() - if k == nil { - logger.DebugF("[%d] Stop kube-proxy: kube proxy object is nil. Skip.\n", startID) return } + logger := k.sshClient.settings.Logger() + if k.stop { logger.DebugF("[%d] Stop kube-proxy: kube proxy already stopped. Skip.\n", startID) return @@ -265,7 +266,7 @@ func (k *KubeProxy) upTunnel( useLocalPort int, tunnelErrorCh chan error, startID int, -) (tun *Tunnel, localPort int, err error) { +) (*Tunnel, int, error) { logger := k.sshClient.settings.Logger() logger.DebugF( @@ -276,7 +277,7 @@ func (k *KubeProxy) upTunnel( ) rewriteLocalPort := false - localPort = useLocalPort + localPort := useLocalPort if useLocalPort < 1 { logger.DebugF( @@ -292,6 +293,7 @@ func (k *KubeProxy) upTunnel( maxRetries := 5 retries := 0 var lastError error + var tun *Tunnel for { logger.DebugF("[%d] Start %d iteration for up tunnel\n", startID, retries) @@ -348,13 +350,13 @@ func (k *KubeProxy) upTunnel( func (k *KubeProxy) runKubeProxy( waitCh chan error, startID int, -) (proxy *SSHCommand, port string, err error) { +) (*SSHCommand, string, error) { logger := k.sshClient.settings.Logger() logger.DebugF("[%d] Begin starting proxy\n", startID) - proxy = k.proxyCMD(startID) + proxy := k.proxyCMD(startID) - port = "" + port := "" portReady := make(chan struct{}, 1) portRe := regexp.MustCompile(`Starting to serve on .*?:(\d+)`) @@ -379,7 +381,7 @@ func (k *KubeProxy) runKubeProxy( }) logger.DebugF("[%d] Start proxy command\n", startID) - err = proxy.Start() + err := proxy.Start() if err != nil { logger.DebugF("[%d] Start proxy command error: %v\n", startID, err) return nil, "", fmt.Errorf("start kubectl proxy: %w", err) diff --git a/pkg/ssh/gossh/kube-proxy_test.go b/pkg/ssh/gossh/kube-proxy_test.go new file mode 100644 index 0000000..5d17aa6 --- /dev/null +++ b/pkg/ssh/gossh/kube-proxy_test.go @@ -0,0 +1,89 @@ +// Copyright 2025 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 gossh + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/deckhouse/lib-dhctl/pkg/retry" + "github.com/stretchr/testify/require" + + sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" +) + +func TestKubeProxy(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestKubeProxy") + + sshClient, container := startContainerAndClientAndKind(t, test) + + cmd := NewSSHCommand(sshClient, "kubectl", "get", "no") + out, err := cmd.CombinedOutput(context.Background()) + test.Logger.InfoF("kubectl get no\n%s", out) + require.NoError(t, err) + + t.Run("Kubeproxy with HealthMonitor", func(t *testing.T) { + kp := sshClient.KubeProxy() + port, err := kp.Start(-1) + require.NoError(t, err) + + checkKubeProxy(t, test, port, false) + + // restart container case + restartSleep := 5 * time.Second + err = container.Container.SoftRestart(true, restartSleep) + require.NoError(t, err) + + // wait for ssh client/tunnel/kubeproxy restart + time.Sleep(20 * time.Second) + checkKubeProxy(t, test, port, false) + + // network issue case + err = container.Container.FailAndUpConnection(restartSleep) + require.NoError(t, err) + + // wait for ssh client/tunnel/kubeproxy restart + time.Sleep(20 * time.Second) + checkKubeProxy(t, test, port, false) + + kp.StopAll() + }) +} + +func checkKubeProxy(t *testing.T, test *sshtesting.Test, localServerPort string, wantError bool) { + url := fmt.Sprintf("http://127.0.0.1:%s/api/v1/nodes", localServerPort) + + requestLoop := retry.NewEmptyParams( + retry.WithName("Check kube proxy available by %s", url), + retry.WithAttempts(10), + retry.WithWait(500*time.Millisecond), + retry.WithLogger(test.Logger), + ) + + _, err := sshtesting.DoGetRequest( + url, + requestLoop, + sshtesting.NewPrefixLogger(test.Logger).WithPrefix(test.FullName()), + ) + + assert := require.NoError + if wantError { + assert = require.Error + } + + assert(t, err, "check local tunnel. Want error %v", wantError) +} diff --git a/pkg/ssh/gossh/reverse-tunnel.go b/pkg/ssh/gossh/reverse-tunnel.go index 629d9b5..509b0f0 100644 --- a/pkg/ssh/gossh/reverse-tunnel.go +++ b/pkg/ssh/gossh/reverse-tunnel.go @@ -181,7 +181,7 @@ func (t *ReverseTunnel) tryToRestart(ctx context.Context, id int, killer connect t.stop(id, false) t.sshClient.settings.Logger().DebugF("[%d] Kill tunnel\n", id) // (k EmptyReverseTunnelKiller) KillTunnel won't return error anyways, so we couldn't check return values - killer.KillTunnel(ctx) + _, _ = killer.KillTunnel(ctx) return t.upNewTunnel(id) } @@ -227,7 +227,7 @@ func (t *ReverseTunnel) StartHealthMonitor(ctx context.Context, checker connecti } go func() { - logger.DebugLn("Start health monitor") + logger.DebugF("Start health monitor") // we need chan for restarting because between restarting we can get stop signal restartCh := make(chan int, 1024) id := -1 @@ -238,14 +238,13 @@ func (t *ReverseTunnel) StartHealthMonitor(ctx context.Context, checker connecti logger.DebugF("[%d] Signal was sent. Chan len: %d\n", id, len(restartCh)) } for { - if !checkReverseTunnel(id) { go restart(id) } select { case <-t.stopCh: - logger.DebugLn("Stop health monitor") + logger.DebugF("Stop health monitor") return case oldId := <-restartCh: restartsCount++ diff --git a/pkg/ssh/gossh/reverse-tunnel_test.go b/pkg/ssh/gossh/reverse-tunnel_test.go index f939dfc..f8c229e 100644 --- a/pkg/ssh/gossh/reverse-tunnel_test.go +++ b/pkg/ssh/gossh/reverse-tunnel_test.go @@ -30,7 +30,7 @@ import ( func TestReverseTunnel(t *testing.T) { test := sshtesting.ShouldNewTest(t, "TestReverseTunnel") - sshClient, container := startContainerAndClientWithContainer(t, test, sshtesting.WithNoWriteSSHDConfig()) + sshClient, container := startContainerAndClientWithContainer(t, test) // we don't have /opt/deckhouse in the container, so we should create it before start any UploadScript with sudo err := container.Container.CreateDeckhouseDirs() @@ -54,7 +54,7 @@ func TestReverseTunnel(t *testing.T) { upTunnelRemoteServerPort := sshtesting.RandPortExclude([]int{containerSSHDPort}) t.Run("Reverse tunnel from container to host", func(t *testing.T) { - remoteServerInvalidPort := sshtesting.RandPortExclude([]int{upTunnelRemoteServerPort, containerSSHDPort}) + // remoteServerInvalidPort := sshtesting.RandPortExclude([]int{upTunnelRemoteServerPort, containerSSHDPort}) localInvalidPort := sshtesting.RandInvalidPortExclude([]int{localServerPort}) cases := []struct { @@ -79,47 +79,52 @@ func TestReverseTunnel(t *testing.T) { title: "Invalid local bind", address: tunnelAddressString(localInvalidPort, containerSSHDPort), wantErr: true, - err: fmt.Sprintf("failed to listen remote on 127.0.0.1:%d", upTunnelRemoteServerPort), - }, - { - title: "Wrong local bind", - address: tunnelAddressString(localServerPort, remoteServerInvalidPort), - wantErr: false, - errFromChan: fmt.Sprintf("Cannot dial to 127.0.0.1:%d", remoteServerInvalidPort), + err: fmt.Sprintf("failed to listen remote on 127.0.0.1:%d", containerSSHDPort), }, + // { + // title: "Wrong local bind", + // address: tunnelAddressString(localServerPort, remoteServerInvalidPort), + // wantErr: false, + // errFromChan: fmt.Sprintf("Cannot dial to 127.0.0.1:%d", remoteServerInvalidPort), + // }, } for _, c := range cases { t.Run(c.title, func(t *testing.T) { tun := NewReverseTunnel(sshClient, c.address) err := tun.Up() - registerStopReverceTunnel(t, tun) - - if c.wantErr { + if !c.wantErr { + if c.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + } + + requestAddress := fmt.Sprintf("http://127.0.0.1:%d%s", upTunnelRemoteServerPort, handler.Path) + + // try to get a response from local web server + cmd := NewSSHCommand(sshClient, "curl", "-m", "4", "-s", requestAddress) + cmd.WithTimeout(6 * time.Second) + out, err := cmd.CombinedOutput(context.Background()) + require.NoError(t, err, "execute remote curl %s", requestAddress) + time.Sleep(5 * time.Second) + if len(c.errFromChan) == 0 { + require.Equal(t, response, string(out)) + } else { + errMsg := <-tun.errorCh + require.Contains(t, errMsg.err.Error(), c.errFromChan) + } + + // try to up again: expecting error + err = tun.Up() require.Error(t, err) - require.Contains(t, err.Error(), c.err) - } - - requestAddress := fmt.Sprintf("http://127.0.0.1:%d%s", upTunnelRemoteServerPort, handler.Path) - - // try to get a response from local web server - cmd := NewSSHCommand(sshClient, "curl", "-m", "4", "-s", requestAddress) - cmd.WithTimeout(6 * time.Second) - out, err := cmd.CombinedOutput(context.Background()) - require.NoError(t, err, "execute remote curl %s", requestAddress) - - if len(c.errFromChan) == 0 { - require.Equal(t, response, string(out)) + require.Equal(t, err.Error(), "already up") + tun.Stop() } else { - errMsg := <-tun.errorCh - require.Contains(t, errMsg.err.Error(), c.errFromChan) + require.Error(t, err) + require.Contains(t, err.Error(), c.err) } - - // try to up again: expecting error - err = tun.Up() - require.Error(t, err) - require.Equal(t, err.Error(), "already up") + tun.Stop() }) } }) @@ -157,7 +162,7 @@ exit $? checkTunnelAction := func() error { out, err := checker.CheckTunnel(context.Background()) if err != nil { - test.Logger.InfoF("Failed to check tunnel: %s %v", out, err) + test.Logger.DebugF("Failed to check tunnel: %s %v", out, err) return err } return nil @@ -177,7 +182,7 @@ exit $? restartSleep := 5 * time.Second tun.StartHealthMonitor(context.Background(), checker, killer) - test.Logger.InfoF( + test.Logger.DebugF( "Waiting %s for tunnel monitor to start. And restart container. Wait %s before start container for fail check", upMonitorSleep.String(), restartSleep.String(), @@ -189,7 +194,7 @@ exit $? err = container.Container.CreateDeckhouseDirs() require.NoError(t, err, "create deckhouse dirs") - test.Logger.InfoF( + test.Logger.DebugF( "Waiting %s for tunnel monitor to restart", upMonitorSleep.String(), ) @@ -202,7 +207,7 @@ exit $? err = retry.NewLoopWithParams(checkLoopAfterRestart).Run(checkTunnelAction) require.NoError(t, err, "tunnel check after restart") - test.Logger.InfoF( + test.Logger.DebugF( "Disconnect (fail connection between server and client) case. Wait %s before connect. Wait %s before check", restartSleep.String(), upMonitorSleep.String(), diff --git a/pkg/ssh/gossh/testing/agent.go b/pkg/ssh/gossh/testing/agent.go index 089b278..778dace 100644 --- a/pkg/ssh/gossh/testing/agent.go +++ b/pkg/ssh/gossh/testing/agent.go @@ -61,6 +61,15 @@ func StartTestAgent(t *testing.T, wrapper *TestContainerWrapper) *Agent { } agent, err := StartAgent(sockDir, wrapper.Settings.Test.Logger, privateKey...) + // fallback to /tmp if unix socket name is too long + if err != nil { + if strings.Contains(err.Error(), "too long for Unix domain socket") { + _ = wrapper.Settings.Test.SetTmpDir("/tmp") + sockDir = wrapper.Settings.Test.TmpDir() + agent, err = StartAgent(sockDir, wrapper.Settings.Test.Logger, privateKey...) + } + } + require.NoError(t, err) agent.RegisterCleanup(t) @@ -106,7 +115,7 @@ func (a *Agent) start() error { out, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("cannot start ssh-agent with sock %s: %w", sock, err) + return fmt.Errorf("cannot start ssh-agent with sock %s: %w: %s", sock, err, string(out)) } pidSubmatches := pidRegex.FindSubmatch(out) @@ -121,24 +130,21 @@ func (a *Agent) start() error { a.pid = pid - a.logInfo("started successfully with pid: %d", a.Pid()) + a.logDebug("started successfully with pid: %d", a.Pid()) go func() { stopCh := a.stopCh - select { - case <-stopCh: - a.logInfo("shutting down ssh-agent") - // Find the process by its PID - process, err := os.FindProcess(a.Pid()) - if err != nil { - a.cleanupAndLog("find process", err) - return - } - - err = process.Signal(syscall.SIGTERM) - a.cleanupAndLog("kill", err) + <-stopCh + a.logDebug("shutting down ssh-agent") + // Find the process by its PID + process, err := os.FindProcess(a.Pid()) + if err != nil { + a.cleanupAndLog("find process", err) return } + + err = process.Signal(syscall.SIGTERM) + a.cleanupAndLog("kill", err) }() return nil @@ -200,7 +206,7 @@ func (a *Agent) RegisterCleanup(t *testing.T) { a.Stop() leaveSocket := retry.NewEmptyParams( - retry.WithName(fmt.Sprintf("Wait socket %s leave", socket)), + retry.WithName("Wait socket %s leave", socket), retry.WithWait(2*time.Second), retry.WithAttempts(10), retry.WithLogger(a.logger), @@ -232,7 +238,7 @@ func (a *Agent) run(stdin string, name string, args ...string) error { cmd.Stdin = strings.NewReader(stdin) } - a.logInfo("run %s with envs: %s", cmd.String(), strings.Join(cmd.Env, " ")) + a.logDebug("run %s with envs: %s", cmd.String(), strings.Join(cmd.Env, " ")) out, err := cmd.CombinedOutput() if err != nil { @@ -255,11 +261,11 @@ func (a *Agent) cleanupAndLog(msg string, err error) { a.mu.Unlock() - a.logInfo("%s success", msg) + a.logDebug("%s success", msg) } -func (a *Agent) logInfo(f string, args ...any) { - a.log(a.logger.InfoF, f, args...) +func (a *Agent) logDebug(f string, args ...any) { + a.log(a.logger.DebugF, f, args...) } func (a *Agent) logError(f string, args ...any) { diff --git a/pkg/ssh/gossh/testing/helpers.go b/pkg/ssh/gossh/testing/helpers.go index 8241f7d..ebb9bdf 100644 --- a/pkg/ssh/gossh/testing/helpers.go +++ b/pkg/ssh/gossh/testing/helpers.go @@ -20,6 +20,7 @@ import ( "encoding/pem" "fmt" "os" + "path/filepath" "testing" "time" @@ -140,3 +141,100 @@ func removeFiles(paths ...string) []error { return removeErrors } + +func PrepareFakeBashibleBundle(t *testing.T, test *Test, entrypoint, bundleDir string) string { + bundleDirPath := func() []string { + return []string{"bundle_test", bundleDir} + } + + parentDir := test.MustMkSubDirs(t, bundleDirPath()...) + + entrypointScript := `#!/bin/bash + +echo "starting execute steps..." + +BUNDLE_STEPS_DIR=/var/lib/bashible/bundle_steps +BOOTSTRAP_DIR=/var/lib/bashible +MAX_RETRIES=5 + +for arg in "$@"; do + if [[ "$arg" == "--add-failure" ]] + then + echo "failures included" + export INCLUDE_FAILURE=true + fi +done + +# Execute bashible steps +for step in $BUNDLE_STEPS_DIR/*; do + echo === + echo === Step: $step + echo === + attempt=0 + sx="" + until /bin/bash --noprofile --norc -"$sx"eEo pipefail -c "export TERM=xterm-256color; unset CDPATH; cd $BOOTSTRAP_DIR; source $step" 2> >(tee /var/lib/bashible/step.log >&2) + do + attempt=$(( attempt + 1 )) + if [ -n "${MAX_RETRIES-}" ] && [ "$attempt" -gt "${MAX_RETRIES}" ]; then + >&2 echo "ERROR: Failed to execute step $step. Retry limit is over." + exit 1 + fi + >&2 echo "Failed to execute step "$step" ... retry in 10 seconds." + sleep 10 + echo === + echo === Step: $step + echo === + if [ "$attempt" -gt 2 ]; then + sx=x + fi + done +done + +` + + entrypointPath := append(bundleDirPath(), entrypoint) + test.MustCreateFile(t, entrypointScript, true, entrypointPath...) + + scrips := []struct { + name string + content string + }{ + { + name: "01-step.sh", + content: `#!/bin/bash +echo "just a step" + +for i in {0..3} +do + sleep $(( $RANDOM % 2 )) + echo $i +done +`, + }, + { + name: "02-step.sh", + content: `#!/bin/bash + +echo "second step" + +for i in {0..4} +do + sleep $(( $RANDOM % 2 )) + echo $i + if [[ $i -gt 2 && $INCLUDE_FAILURE == "true" ]] + then + echo "oops! failure!" + exit 1 + fi +done +`, + }, + } + + for _, c := range scrips { + scriptPath := append(bundleDirPath(), "bundle_steps", c.name) + test.MustCreateFile(t, c.content, true, scriptPath...) + } + + return filepath.Dir(parentDir) +} diff --git a/pkg/ssh/gossh/testing/kind.go b/pkg/ssh/gossh/testing/kind.go new file mode 100644 index 0000000..f5f2b1a --- /dev/null +++ b/pkg/ssh/gossh/testing/kind.go @@ -0,0 +1,89 @@ +// 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 ssh_testing + +import ( + "fmt" + "os" + "os/exec" + "time" + + "github.com/deckhouse/lib-dhctl/pkg/retry" +) + +const ( + KindConfigPath = "../../../hack/kind/cluster-kube-proxy.yml" + KindClusterName = "k8s-test" + KindBinary = "../../../bin/kind" +) + +func CreateKINDCluster() error { + // checking out, what kind config exists + _, err := os.Stat(KindConfigPath) + if err != nil { + return err + } + // args to command + args := []string{"create", "cluster", "--name=" + KindClusterName, "--config=" + KindConfigPath} + cmd := exec.Command(KindBinary, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("could not create kind cluster: %s: %w\n", out, err) + } + + return err +} + +func DeleteKindCluster() error { + args := []string{"delete", "cluster", "--name=" + KindClusterName} + cmd := exec.Command(KindBinary, args...) + + return cmd.Run() +} + +func GetKINDControlPlaneIP() (string, error) { + getIPCmd := []string{ + "inspect", + "-f", "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", + KindClusterName + "-control-plane", + } + ip := "" + + err := retry.NewSilentLoop("discovering IP of control plane noe", 10, 2*time.Second).Run(func() error { + cmd := exec.Command("docker", getIPCmd...) + out, err := cmd.Output() + if err != nil { + return err + } + ip = string(out) + return nil + }) + if err != nil { + return "", err + } + + return ip, nil +} + +func GetKINDKubeconfig() (string, error) { + args := []string{"get", "kubeconfig", "--name=" + KindClusterName} + cmd := exec.Command(KindBinary, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("couldn't get kind kubeconfig: %s: %w", string(out), err) + } + + return string(out), nil +} diff --git a/pkg/ssh/gossh/testing/setttings.go b/pkg/ssh/gossh/testing/setttings.go index 435fd20..9d897c4 100644 --- a/pkg/ssh/gossh/testing/setttings.go +++ b/pkg/ssh/gossh/testing/setttings.go @@ -17,9 +17,10 @@ package ssh_testing import ( "strconv" + "github.com/deckhouse/lib-dhctl/pkg/log" + "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/session" - "github.com/deckhouse/lib-dhctl/pkg/log" ) func TestLogger() *log.InMemoryLogger { @@ -37,6 +38,17 @@ func CreateDefaultTestSettings(test *Test) settings.Settings { return settings.NewBaseProviders(getDefaultParams(test)) } +func getParamsNoDebug(test *Test) settings.ProviderParams { + return settings.ProviderParams{ + LoggerProvider: log.SimpleLoggerProvider(test.Logger), + IsDebug: false, + } +} + +func CreateTestSettingNoDebug(test *Test) settings.Settings { + return settings.NewBaseProviders(getParamsNoDebug(test)) +} + func CreateDefaultTestSettingsWithAgent(test *Test, agentSockPath string) settings.Settings { params := getDefaultParams(test) params.AuthSock = agentSockPath diff --git a/pkg/ssh/gossh/testing/ssh_container.go b/pkg/ssh/gossh/testing/ssh_container.go index be0f711..70f3f97 100644 --- a/pkg/ssh/gossh/testing/ssh_container.go +++ b/pkg/ssh/gossh/testing/ssh_container.go @@ -154,7 +154,7 @@ AllowTcpForwarding yes } func (c *SSHContainer) Start(waitSSHDStarted bool) error { - c.logInfo("Starting container fully...") + c.logDebug("Starting container fully...") err := c.createNetwork() if err != nil { @@ -174,7 +174,7 @@ func (c *SSHContainer) Start(waitSSHDStarted bool) error { return err } - c.logInfo("Container fully started %s", c.ShortContainerId()) + c.logDebug("Container fully started %s", c.ShortContainerId()) return nil } @@ -189,9 +189,34 @@ func (c *SSHContainer) Restart(waitSSHDStarted bool, sleepBeforeStart time.Durat return c.startContainer(waitSSHDStarted) } +func (c *SSHContainer) SoftRestart(waitSSHDStarted bool, sleepBeforeStart time.Duration) error { + if err := c.isContainerStarted("stop container"); err != nil { + return nil + } + + description := func(name string) string { + return fmt.Sprintf("%s %s", name, c.GetContainerId()) + } + + id := c.GetContainerId() + shortID := c.ShortContainerId() + + c.logDebug("Stop container %s...", shortID) + + if err := c.runDocker(description("stop container"), "stop", id); err != nil { + return err + } + + Sleep(sleepBeforeStart) + + c.logDebug("Start container %s...", shortID) + + return c.runDocker(description("start container"), "start", id) +} + func (c *SSHContainer) Stop() error { shortId := c.ShortContainerId() - c.logInfo("Stopping container '%s' fully...", shortId) + c.logDebug("Stopping container '%s' fully...", shortId) resError := "" if err := c.stopContainer(); err != nil { @@ -206,7 +231,7 @@ func (c *SSHContainer) Stop() error { return c.wrapError("cannot fully stop container: %v", errors.New(resError)) } - c.logInfo("Container '%s' fully stopped", shortId) + c.logDebug("Container '%s' fully stopped", shortId) return nil } @@ -222,11 +247,11 @@ func (c *SSHContainer) FailAndUpConnection(sleepBeforeConnect time.Duration) err } func (c *SSHContainer) Disconnect() error { - return c.runDockerNetworkConnect(false) + return c.runDockerNetworkConnect(true) } func (c *SSHContainer) Connect() error { - return c.runDockerNetworkConnect(true) + return c.runDockerNetworkConnect(false) } func (c *SSHContainer) ExecToContainer(description string, command ...string) error { @@ -262,6 +287,23 @@ func (c *SSHContainer) CreateDeckhouseDirs() error { return c.ExecToContainer(description("set mode"), "chmod", "-R", "777", nodeTmpPath) } +func (c *SSHContainer) CreateDirectory(path string) error { + description := func(name string) string { + d := "any directory inside a container" + if name == "" { + return d + } + + return fmt.Sprintf("%s %s", d, name) + } + + if err := c.ExecToContainer(description("create"), "mkdir", "-p", path); err != nil { + return err + } + + return c.ExecToContainer(description("chown"), "chown", "-R", c.settings.Username+":"+c.settings.Username, path) +} + func (c *SSHContainer) WithExternalNetwork(network string) *SSHContainer { c.network = network c.externalNetwork = true @@ -419,7 +461,7 @@ func (c *SSHContainer) runContainerArgs() []string { func (c *SSHContainer) startContainer(waitSSHDStarted bool) error { cmd := append([]string{"run"}, c.runContainerArgs()...) - c.logInfo("Starting container...") + c.logDebug("Starting container...") id, err := c.runDockerWithOut("start container", cmd...) if err != nil { @@ -436,7 +478,7 @@ func (c *SSHContainer) startContainer(waitSSHDStarted bool) error { return err } - c.logInfo("Container started: ID: %s IP: %s", c.ShortContainerId(), c.GetContainerIP()) + c.logDebug("Container started: ID: %s IP: %s", c.ShortContainerId(), c.GetContainerIP()) if !waitSSHDStarted { return nil @@ -451,7 +493,7 @@ func (c *SSHContainer) startContainer(waitSSHDStarted bool) error { return err } if err := conn.Close(); err != nil { - c.ContainerSettings().Logger.InfoF("Failed to close SSHD connection after restart container: %v", err) + c.ContainerSettings().Logger.DebugF("Failed to close SSHD connection after restart container: %v", err) } return nil @@ -479,13 +521,13 @@ func (c *SSHContainer) stopContainer() error { id := c.GetContainerId() shortID := c.ShortContainerId() - c.logInfo("Stop container %s...", shortID) + c.logDebug("Stop container %s...", shortID) if err := c.runDocker(description("stop container"), "stop", id); err != nil { return err } - c.logInfo("Remove container %s...", shortID) + c.logDebug("Remove container %s...", shortID) return c.runDocker(description("remove container"), "rm", id) } @@ -512,7 +554,7 @@ func (c *SSHContainer) createNetwork() error { if hasNetwork { if isExternal { - c.logInfo("Skip creating network '%s'. Has external network", c.GetNetwork()) + c.logDebug("Skip creating network '%s'. Has external network", c.GetNetwork()) // do not need to create network return nil } @@ -522,7 +564,7 @@ func (c *SSHContainer) createNetwork() error { network := c.generateDockerNetworkName() - c.logInfo("Creating network '%s'...", network) + c.logDebug("Creating network '%s'...", network) description := fmt.Sprintf("create network '%s'", network) if err := c.runDocker(description, "network", "create", network); err != nil { @@ -541,11 +583,11 @@ func (c *SSHContainer) removeNetwork() error { network := c.GetNetwork() if !hasNetwork || isExternal { - c.logInfo("Skip deleting network '%s'. Has external network or empty", network) + c.logDebug("Skip deleting network '%s'. Has external network or empty", network) return nil } - c.logInfo("Deleting network %s...", network) + c.logDebug("Deleting network %s...", network) if err := c.runDocker(fmt.Sprintf("remove network %s", network), "network", "rm", network); err != nil { return err @@ -556,9 +598,9 @@ func (c *SSHContainer) removeNetwork() error { return nil } -func (c *SSHContainer) logInfo(format string, args ...any) { +func (c *SSHContainer) logDebug(format string, args ...any) { format += fmt.Sprintf(" (%s)", c.settings.String()) - c.settings.Logger.InfoF(format, args...) + c.settings.Logger.DebugF(format, args...) } func (c *SSHContainer) runDockerNetworkConnect(isDisconnect bool) error { @@ -579,7 +621,7 @@ func (c *SSHContainer) runDockerNetworkConnect(isDisconnect bool) error { network := c.GetNetwork() - c.logInfo( + c.logDebug( "%s network %s to container %s...", strings.ToTitle(cmdName), network, @@ -589,6 +631,32 @@ func (c *SSHContainer) runDockerNetworkConnect(isDisconnect bool) error { return c.runDocker(cmdName, "network", cmdName, network, c.GetContainerId()) } +func (c *SSHContainer) DockerNetworkConnect(isDisconnect bool, name string) error { + cmdName := "connect" + if isDisconnect { + cmdName = "disconnect" + } + + description := fmt.Sprintf("network %s", cmdName) + + if err := c.isContainerStarted(description); err != nil { + return err + } + + if err := c.hasNetwork(description); err != nil { + return err + } + + c.logDebug( + "%s network %s to container %s...", + strings.ToTitle(cmdName), + name, + c.ShortContainerId(), + ) + + return c.runDocker(cmdName, "network", cmdName, name, c.GetContainerId()) +} + func (c *SSHContainer) discoveryContainerIP() (string, error) { description := "Getting IP address of container" if err := c.hasNetwork(description); err != nil { @@ -635,9 +703,23 @@ func (c *SSHContainer) defaultRetryParams(name string) retry.Params { logger := c.ContainerSettings().Test.Logger return retry.NewEmptyParams( - retry.WithName(name), + retry.WithName("%s", name), retry.WithAttempts(5), retry.WithWait(3*time.Second), retry.WithLogger(logger), ) } + +func (c *SSHContainer) DownloadKubectl(version string) error { + args := []string{"curl", "-LO", "https://dl.k8s.io/release/" + version + "/bin/linux/amd64/kubectl"} + + if err := c.ExecToContainer("kubectl", args...); err != nil { + return err + } + + if err := c.ExecToContainer("move kubectl to /usr/local/bin", "mv", "kubectl", "/usr/local/bin/"); err != nil { + return err + } + + return c.ExecToContainer("set execute permission on kubectl", "chmod", "+x", "/usr/local/bin/kubectl") +} diff --git a/pkg/ssh/gossh/testing/test.go b/pkg/ssh/gossh/testing/test.go index 172f8d2..f233e05 100644 --- a/pkg/ssh/gossh/testing/test.go +++ b/pkg/ssh/gossh/testing/test.go @@ -279,7 +279,7 @@ func (s *Test) Cleanup(t *testing.T) { } if !govalue.Nil(s.Logger) { - s.Logger.InfoF("Temp dir '%s' removed for test %s", tmpDir, s.FullName()) + s.Logger.DebugF("Temp dir '%s' removed for test %s", tmpDir, s.FullName()) } } @@ -296,3 +296,31 @@ func (s *Test) fileNameAndSubDirs(pathInTestDir ...string) (string, []string) { func addRandomSuffix(name string, suffix string) string { return fmt.Sprintf("%s%s%s", name, randomSuffixSeparator, suffix) } + +func (s *Test) SetTmpDir(dir string) error { + stats, err := os.Stat(dir) + if err != nil { + return err + } + + if !stats.IsDir() { + return fmt.Errorf("%s is not a directory", dir) + } + localTmpDirStr := filepath.Join(dir, tmpGlobalDirName, s.id) + + err = os.MkdirAll(localTmpDirStr, 0777) + if err != nil { + return fmt.Errorf("failed to create local tmp dir %s: %v", localTmpDirStr, err) + } + + s.tmpDir = localTmpDirStr + return nil +} + +func (s *Test) MustCreateUnaccessibleDir(t *testing.T, name string) { + fullName := filepath.Join(s.tmpDir, name) + require.NoDirExists(t, fullName) + + err := os.MkdirAll(fullName, 0o100) + require.NoError(t, err) +} diff --git a/pkg/ssh/gossh/testing/test_container_wrapper.go b/pkg/ssh/gossh/testing/test_container_wrapper.go index 350076e..ec023ae 100644 --- a/pkg/ssh/gossh/testing/test_container_wrapper.go +++ b/pkg/ssh/gossh/testing/test_container_wrapper.go @@ -17,9 +17,10 @@ package ssh_testing import ( "testing" - "github.com/deckhouse/lib-connection/pkg/ssh/session" "github.com/name212/govalue" "github.com/stretchr/testify/require" + + "github.com/deckhouse/lib-connection/pkg/ssh/session" ) type TestContainerWrapperSettingsOpts func(container *TestContainerWrapperSettings) @@ -58,11 +59,6 @@ func NewTestContainerWrapper(t *testing.T, test *Test, opts ...TestContainerWrap opt(testSettings) } - logger := testSettings.Logger - if govalue.Nil(logger) { - logger = TestLogger() - } - testContainer := &TestContainerWrapper{ Settings: testSettings, } @@ -80,7 +76,7 @@ func NewTestContainerWrapper(t *testing.T, test *Test, opts ...TestContainerWrap require.NoError(t, err) } - test.Logger.InfoF("Private key created: path '%s' pub key path: %s", privateKeyPath, publicKeyPath) + test.Logger.DebugF("Private key created: path '%s' pub key path: %s", privateKeyPath, publicKeyPath) testSettings.PublicKey = &PublicKey{ Path: publicKeyPath, diff --git a/pkg/ssh/gossh/testing/web_server.go b/pkg/ssh/gossh/testing/web_server.go index 0eeb343..ba6bb4b 100644 --- a/pkg/ssh/gossh/testing/web_server.go +++ b/pkg/ssh/gossh/testing/web_server.go @@ -150,7 +150,7 @@ func (s *HTTPServer) Start(waitStart bool) error { url := fmt.Sprintf("http://%s%s", s.address, HealthzPath) loop := retry.NewEmptyParams( - retry.WithName(fmt.Sprintf("Check HTTP server %s started", s.logger.prefix)), + retry.WithName("Check HTTP server %s started", s.logger.prefix), retry.WithAttempts(10), retry.WithWait(500*time.Millisecond), retry.WithLogger(s.logger.Logger), diff --git a/pkg/ssh/gossh/tunnel.go b/pkg/ssh/gossh/tunnel.go index 5f62da7..833dab4 100644 --- a/pkg/ssh/gossh/tunnel.go +++ b/pkg/ssh/gossh/tunnel.go @@ -117,14 +117,12 @@ func (t *Tunnel) acceptTunnelConnection(id int, remoteAddress string, listener n if err != nil { t.errorCh <- err } - }() _, err := io.Copy(localConn, remoteConn) if err != nil { t.errorCh <- err } - }() } } diff --git a/pkg/ssh/gossh/tunnel_test.go b/pkg/ssh/gossh/tunnel_test.go index f1143df..b15c9e0 100644 --- a/pkg/ssh/gossh/tunnel_test.go +++ b/pkg/ssh/gossh/tunnel_test.go @@ -30,7 +30,7 @@ import ( func TestTunnel(t *testing.T) { test := sshtesting.ShouldNewTest(t, "TestTunnel") - sshClient, container := startContainerAndClientWithContainer(t, test, sshtesting.WithNoWriteSSHDConfig()) + sshClient, container := startContainerAndClientWithContainer(t, test) sshClient.WithLoopsParams(ClientLoopsParams{ NewSession: retry.NewEmptyParams( retry.WithAttempts(5), @@ -79,58 +79,52 @@ done`, remoteServerPort) localServerPort := sshtesting.RandPortExclude(localsReservedPorts) localsReservedPorts = append(localsReservedPorts, localServerPort) - localServerInvalidPort := sshtesting.RandInvalidPortExclude(localsReservedPorts) + // localServerInvalidPort := sshtesting.RandInvalidPortExclude(localsReservedPorts) remoteServerInvalidPort := sshtesting.RandPortExclude([]int{remoteServerPort, container.Container.RemotePort()}) cases := []struct { title string - localPort int - remotePort int + address string wantErr bool err string }{ { - title: "Tunnel, success", - localPort: localServerPort, - remotePort: remoteServerPort, - wantErr: false, + title: "Tunnel, success", + address: tunnelAddressString(localServerPort, remoteServerPort), + wantErr: false, }, { - title: "Invalid address", - localPort: localServerPort, - remotePort: remoteServerInvalidPort, - wantErr: true, - err: "invalid address must be 'remote_bind:remote_port:local_bind:local_port'", + title: "Invalid address", + address: fmt.Sprintf("%d:127.0.0.1:%d", remoteServerInvalidPort, localServerPort), + wantErr: true, + err: "invalid address must be 'remote_bind:remote_port:local_bind:local_port'", }, { - title: "Invalid local bind", - localPort: localServerInvalidPort, - remotePort: remoteServerPort, - wantErr: true, - err: fmt.Sprintf("failed to listen local on 127.0.0.1:%d", localServerInvalidPort), + title: "Invalid local bind", + address: tunnelAddressString(22, remoteServerPort), + wantErr: true, + err: fmt.Sprintf("failed to listen local on 127.0.0.1:%d", 22), }, } for _, c := range cases { t.Run(c.title, func(t *testing.T) { - address := tunnelAddressString(c.localPort, c.remotePort) - tun := NewTunnel(sshClient, address) + tun := NewTunnel(sshClient, c.address) err = tun.Up() registerStopTunnel(t, tun) - if c.wantErr { + if !c.wantErr { + checkLocalTunnel(t, test, localServerPort, false) + // try to up again: expecting error + err = tun.Up() + require.Error(t, err) + require.Equal(t, err.Error(), "already up") + } else { require.Error(t, err) require.Contains(t, err.Error(), c.err) } - - checkLocalTunnel(t, test, localServerPort, false) - - // try to up again: expectiong error - err = tun.Up() - require.Error(t, err) - require.Equal(t, err.Error(), "already up") }) } }) @@ -166,47 +160,42 @@ done`, remoteServerPort) checkLocalTunnel(t, test, localServerPort, true) msg := "" - select { - case m, ok := <-errChan: - if !ok { - msg = "monitor channel closed" - } else { - if m != nil { - msg = m.Error() - } - } - - default: - msg = "" + m, ok := <-errChan + if !ok { + msg = "monitor channel closed" + } else if m != nil { + msg = m.Error() } require.Contains(t, msg, fmt.Sprintf("Cannot dial to %s", remoteStr), "got: '%s'", msg) }) - t.Run("Restart connection", func(t *testing.T) { - localServerPort := sshtesting.RandPortExclude(localsReservedPorts) - localsReservedPorts = append(localsReservedPorts, localServerPort) + // this case is incorrect and must be excluded + // HealthMonitor doesn't implement tunnel restarts, it just indecates failures and send messages to channel + // t.Run("Restart connection", func(t *testing.T) { + // localServerPort := sshtesting.RandPortExclude(localsReservedPorts) + // localsReservedPorts = append(localsReservedPorts, localServerPort) - upTunnelWithMonitor(t, tunnelAddressString(localServerPort, remoteServerPort)) + // upTunnelWithMonitor(t, tunnelAddressString(localServerPort, remoteServerPort)) - checkLocalTunnel(t, test, localServerPort, false) + // checkLocalTunnel(t, test, localServerPort, false) - restartSleep := 5 * time.Second - upMonitorSleep := 2 * time.Second + // restartSleep := 5 * time.Second + // upMonitorSleep := 2 * time.Second - test.Logger.InfoF( - "Disconnect (fail connection between server and client) case. Wait %s before connect. Wait %s before check", - restartSleep.String(), - upMonitorSleep.String(), - ) + // test.Logger.DebugF( + // "Disconnect (fail connection between server and client) case. Wait %s before connect. Wait %s before check", + // restartSleep.String(), + // upMonitorSleep.String(), + // ) - err = container.Container.FailAndUpConnection(restartSleep) - require.NoError(t, err) + // err = container.Container.FailAndUpConnection(restartSleep) + // require.NoError(t, err) - time.Sleep(upMonitorSleep) + // time.Sleep(upMonitorSleep) - checkLocalTunnel(t, test, localServerPort, false) - }) + // checkLocalTunnel(t, test, localServerPort, false) + // }) }) } @@ -214,7 +203,7 @@ func checkLocalTunnel(t *testing.T, test *sshtesting.Test, localServerPort int, url := fmt.Sprintf("http://127.0.0.1:%d", localServerPort) requestLoop := retry.NewEmptyParams( - retry.WithName(fmt.Sprintf("Check local tunnel available by %s", url)), + retry.WithName("Check local tunnel available by %s", url), retry.WithAttempts(10), retry.WithWait(500*time.Millisecond), retry.WithLogger(test.Logger), diff --git a/pkg/ssh/gossh/upload-script.go b/pkg/ssh/gossh/upload-script.go index 6d81b24..34a9ab2 100644 --- a/pkg/ssh/gossh/upload-script.go +++ b/pkg/ssh/gossh/upload-script.go @@ -25,10 +25,10 @@ import ( "time" "al.essio.dev/pkg/shellescape" - "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-dhctl/pkg/log" gossh "github.com/deckhouse/lib-gossh" + "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/utils" "github.com/deckhouse/lib-connection/pkg/ssh/utils/tar" ) @@ -105,14 +105,14 @@ func (u *SSHUploadScript) WithExecuteUploadDir(dir string) { u.uploadDir = dir } -func (u *SSHUploadScript) Execute(ctx context.Context) (stdout []byte, err error) { +func (u *SSHUploadScript) Execute(ctx context.Context) ([]byte, error) { logger := u.sshClient.settings.Logger() scriptName := filepath.Base(u.ScriptPath) remotePath := utils.ExecuteRemoteScriptPath(u, scriptName, false) logger.DebugF("Uploading script %s to %s\n", u.ScriptPath, remotePath) - err = NewSSHFile(u.sshClient.settings, u.sshClient.sshClient).Upload(ctx, u.ScriptPath, remotePath) + err := NewSSHFile(u.sshClient.settings, u.sshClient.sshClient).Upload(ctx, u.ScriptPath, remotePath) if err != nil { return nil, fmt.Errorf("upload: %v", err) } @@ -180,23 +180,23 @@ func (u *SSHUploadScript) pathWithEnv(path string) string { var ErrBashibleTimeout = errors.New("Timeout bashible step running") -func (u *SSHUploadScript) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) (stdout []byte, err error) { +func (u *SSHUploadScript) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) ([]byte, error) { logger := u.sshClient.settings.Logger() bundleName := fmt.Sprintf("bundle-%s.tar", time.Now().Format("20060102-150405")) bundleLocalFilepath := filepath.Join(u.sshClient.settings.TmpDir(), bundleName) // tar cpf bundle.tar -C /tmp/dhctl.1231qd23/var/lib bashible - err = tar.CreateTar(bundleLocalFilepath, parentDir, bundleDir) + err := tar.CreateTar(bundleLocalFilepath, parentDir, bundleDir) if err != nil { return nil, fmt.Errorf("tar bundle: %v", err) } // todo - //tomb.RegisterOnShutdown( + // tomb.RegisterOnShutdown( // "Delete bashible bundle folder", // func() { _ = os.Remove(bundleLocalFilepath) }, - //) + // ) // upload to node's deckhouse tmp directory err = NewSSHFile(u.sshClient.settings, u.sshClient.sshClient). diff --git a/pkg/ssh/gossh/upload-script_test.go b/pkg/ssh/gossh/upload-script_test.go index 7148dce..8deb9f6 100644 --- a/pkg/ssh/gossh/upload-script_test.go +++ b/pkg/ssh/gossh/upload-script_test.go @@ -117,7 +117,7 @@ fi } out, err := s.Execute(context.Background()) - if !c.wantErr { + if c.wantErr { require.Error(t, err) require.Contains(t, err.Error(), c.err) return @@ -128,7 +128,6 @@ fi }) } }) - } func TestUploadScriptExecuteBundle(t *testing.T) { @@ -148,7 +147,7 @@ func TestUploadScriptExecuteBundle(t *testing.T) { const entrypoint = "test.sh" - testDir := prepareFakeBashibleBundle(t, test, entrypoint, "bashible") + testDir := sshtesting.PrepareFakeBashibleBundle(t, test, entrypoint, "bashible") t.Run("Upload and execute bundle to container via existing ssh client", func(t *testing.T) { cases := []struct { @@ -199,14 +198,12 @@ func TestUploadScriptExecuteBundle(t *testing.T) { for _, c := range cases { t.Run(c.title, func(t *testing.T) { s := sshClient.UploadScript(entrypoint, c.scriptArgs...) - parentDir := c.parentDir - bundleDir := c.bundleDir if c.prepareFunc != nil { err = c.prepareFunc() require.NoError(t, err) } - _, err := s.ExecuteBundle(context.Background(), parentDir, bundleDir) + _, err := s.ExecuteBundle(context.Background(), c.parentDir, c.bundleDir) if c.wantErr { require.Error(t, err) require.Contains(t, err.Error(), c.err) @@ -218,100 +215,3 @@ func TestUploadScriptExecuteBundle(t *testing.T) { } }) } - -func prepareFakeBashibleBundle(t *testing.T, test *sshtesting.Test, entrypoint, bundleDir string) string { - bundleDirPath := func() []string { - return []string{"bundle_test", bundleDir} - } - - parentDir := test.MustMkSubDirs(t, bundleDirPath()...) - - entrypointScript := `#!/bin/bash - -echo "starting execute steps..." - -BUNDLE_STEPS_DIR=/var/lib/bashible/bundle_steps -BOOTSTRAP_DIR=/var/lib/bashible -MAX_RETRIES=5 - -for arg in "$@"; do - if [[ "$arg" == "--add-failure" ]] - then - echo "failures included" - export INCLUDE_FAILURE=true - fi -done - -# Execute bashible steps -for step in $BUNDLE_STEPS_DIR/*; do - echo === - echo === Step: $step - echo === - attempt=0 - sx="" - until /bin/bash --noprofile --norc -"$sx"eEo pipefail -c "export TERM=xterm-256color; unset CDPATH; cd $BOOTSTRAP_DIR; source $step" 2> >(tee /var/lib/bashible/step.log >&2) - do - attempt=$(( attempt + 1 )) - if [ -n "${MAX_RETRIES-}" ] && [ "$attempt" -gt "${MAX_RETRIES}" ]; then - >&2 echo "ERROR: Failed to execute step $step. Retry limit is over." - exit 1 - fi - >&2 echo "Failed to execute step "$step" ... retry in 10 seconds." - sleep 10 - echo === - echo === Step: $step - echo === - if [ "$attempt" -gt 2 ]; then - sx=x - fi - done -done - -` - - entrypointPath := append(bundleDirPath(), entrypoint) - test.MustCreateFile(t, entrypointScript, true, entrypointPath...) - - scrips := []struct { - name string - content string - }{ - { - name: "01-step.sh", - content: `#!/bin/bash -echo "just a step" - -for i in {0..3} -do - sleep $(( $RANDOM % 2 )) - echo $i -done -`, - }, - { - name: "02-step.sh", - content: `#!/bin/bash - -echo "second step" - -for i in {0..4} -do - sleep $(( $RANDOM % 2 )) - echo $i - if [[ $i -gt 2 && $INCLUDE_FAILURE == "true" ]] - then - echo "oops! failure!" - exit 1 - fi -done -`, - }, - } - - for _, c := range scrips { - scriptPath := append(bundleDirPath(), "bundle_steps", c.name) - test.MustCreateFile(t, c.content, true, scriptPath...) - } - - return parentDir -} diff --git a/pkg/ssh/local/node.go b/pkg/ssh/local/node.go index 0d4deb0..fc175d7 100644 --- a/pkg/ssh/local/node.go +++ b/pkg/ssh/local/node.go @@ -39,8 +39,8 @@ func NewNodeInterface(sett settings.Settings) *NodeInterface { func (n *NodeInterface) Command(name string, args ...string) connection.Command { logger := n.settings.Logger() - logger.DebugLn("Starting NodeInterface.Command") - defer logger.DebugLn("Stop NodeInterface.Command") + logger.DebugF("Starting NodeInterface.Command") + defer logger.DebugF("Stop NodeInterface.Command") return NewCommand(n.settings, name, args...) } @@ -52,8 +52,8 @@ func (n *NodeInterface) File() connection.File { func (n *NodeInterface) UploadScript(scriptPath string, args ...string) connection.Script { logger := n.settings.Logger() - logger.DebugLn("Starting NodeInterface.UploadScript") - defer logger.DebugLn("Stop NodeInterface.UploadScript") + logger.DebugF("Starting NodeInterface.UploadScript") + defer logger.DebugF("Stop NodeInterface.UploadScript") return NewScript(n.settings, scriptPath, args...) } diff --git a/pkg/ssh/local/script.go b/pkg/ssh/local/script.go index 70575c0..5abc6f6 100644 --- a/pkg/ssh/local/script.go +++ b/pkg/ssh/local/script.go @@ -46,7 +46,7 @@ func NewScript(sett settings.Settings, path string, args ...string) *Script { } } -func (s *Script) Execute(ctx context.Context) (stdout []byte, err error) { +func (s *Script) Execute(ctx context.Context) ([]byte, error) { cmd := NewCommand(s.settings, s.scriptPath, s.args...) if s.sudo { cmd.Sudo(ctx) @@ -66,24 +66,24 @@ func (s *Script) Execute(ctx context.Context) (stdout []byte, err error) { defer os.Remove(cmd.program) } - err = cmd.Run(ctx) + err := cmd.Run(ctx) if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { exitErr.Stderr = cmd.StderrBytes() } - err = fmt.Errorf("Execute locally failed: %w", err) + return nil, fmt.Errorf("Execute locally failed: %w", err) } return cmd.StdoutBytes(), nil } -func (s *Script) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) (stdout []byte, err error) { +func (s *Script) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) ([]byte, error) { srcPath := filepath.Join(parentDir, bundleDir) dstPath := filepath.Join("/var/lib/", bundleDir) _ = os.RemoveAll(dstPath) // Cleanup from previous runs - if err = copyRecursively(srcPath, dstPath); err != nil { + if err := copyRecursively(srcPath, dstPath); err != nil { return nil, fmt.Errorf("copy bundle to /var/lib/%s: %w", bundleDir, err) } @@ -101,7 +101,7 @@ func (s *Script) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) cmd.Sudo(ctx) } - if err = cmd.Run(ctx); err != nil { + if err := cmd.Run(ctx); err != nil { s.settings.Logger().DebugF("Execute bundle failed: stdout: %s\n\nstderr: %s\n", cmd.StdoutBytes(), cmd.StderrBytes()) return nil, fmt.Errorf("Execute bundle failed: %w", err) } diff --git a/pkg/ssh/session/session.go b/pkg/ssh/session/session.go index 5afb157..6276247 100644 --- a/pkg/ssh/session/session.go +++ b/pkg/ssh/session/session.go @@ -318,7 +318,8 @@ func (s *Session) selectNewHost() { } host := hosts[hostIndx] - s.remainingHosts = append(hosts[:hostIndx], hosts[hostIndx+1:]...) + s.remainingHosts = hosts[:hostIndx] + s.remainingHosts = append(s.remainingHosts, hosts[hostIndx+1:]...) s.host = host.Host } diff --git a/pkg/ssh/session/session_test.go b/pkg/ssh/session/session_test.go index 3d9c7fe..ca09873 100644 --- a/pkg/ssh/session/session_test.go +++ b/pkg/ssh/session/session_test.go @@ -131,7 +131,8 @@ func TestSession_ChoiceNewHost(t *testing.T) { var remainedHosts []Host for i, host := range availableHosts { if host.Host == ses.host { - remainedHosts = append(availableHosts[:i], availableHosts[i+1:]...) + remainedHosts = availableHosts[:i] + remainedHosts = append(remainedHosts, availableHosts[i+1:]...) break } } diff --git a/pkg/ssh/testssh/client.go b/pkg/ssh/testssh/client.go index 851e09b..8cbf323 100644 --- a/pkg/ssh/testssh/client.go +++ b/pkg/ssh/testssh/client.go @@ -461,16 +461,9 @@ func (c *Client) IsStopped() bool { return c.stopped } -func (c *Client) appendProxy(p *kubeProxy) { - c.mu.Lock() - defer c.mu.Unlock() - - c.kubeProxies = append(c.kubeProxies, p) -} - type kubeProxy struct{} -func (k *kubeProxy) Start(useLocalPort int) (port string, err error) { +func (k *kubeProxy) Start(useLocalPort int) (string, error) { i := rand.New(rand.NewSource(time.Now().UnixNano())).Int() return fmt.Sprintf("%d", i), nil } @@ -538,11 +531,11 @@ func (t *Script) WithRun(f func()) *Script { return t } -func (t *Script) Execute(context.Context) (stdout []byte, err error) { +func (t *Script) Execute(context.Context) ([]byte, error) { return t.execute() } -func (t *Script) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) (stdout []byte, err error) { +func (t *Script) ExecuteBundle(ctx context.Context, parentDir, bundleDir string) ([]byte, error) { return t.execute() } @@ -555,7 +548,7 @@ func (t *Script) WithEnvs(envs map[string]string) {} func (t *Script) WithCleanupAfterExec(doCleanup bool) {} func (t *Script) WithCommanderMode(enabled bool) {} func (t *Script) WithExecuteUploadDir(dir string) {} -func (t *Script) execute() (stdout []byte, err error) { +func (t *Script) execute() ([]byte, error) { if t.handler != nil { t.handler(string(t.stdOut)) } @@ -602,6 +595,12 @@ func (t *Command) Run(ctx context.Context) error { return t.doRun() } +func (t *Command) Start() error { + return t.doRun() +} + +func (t *Command) Stop() {} + func (t *Command) Cmd(ctx context.Context) {} func (t *Command) Sudo(ctx context.Context) {} diff --git a/pkg/ssh/testssh/command_test.go b/pkg/ssh/testssh/command_test.go new file mode 100644 index 0000000..747426f --- /dev/null +++ b/pkg/ssh/testssh/command_test.go @@ -0,0 +1,426 @@ +// Copyright 2025 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 testssh + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/deckhouse/lib-connection/pkg" + "github.com/deckhouse/lib-connection/pkg/ssh/clissh" + "github.com/deckhouse/lib-connection/pkg/ssh/gossh" + sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" + "github.com/deckhouse/lib-connection/pkg/ssh/session" +) + +func TestCommandOutput(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestCommandOutput") + + container := sshtesting.NewTestContainerWrapper(t, test) + sess := sshtesting.Session(container) + keys := container.AgentPrivateKeys() + + t.Run("Get command Output", func(t *testing.T) { + cases := []struct { + title string + command string + args []string + expectedOutput string + timeout time.Duration + wantErr bool + err string + }{ + { + title: "Just echo, success", + command: "echo", + args: []string{`"test output"`}, + expectedOutput: "test output\n", + wantErr: false, + }, + { + title: "With context", + command: `while true; do echo "test"; sleep 5; done`, + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", + }, + { + title: "Command return error", + command: "cat", + args: []string{`"/etc/sudoers"`}, + wantErr: true, + err: "status 1", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + ctx, ctx2, cancel, cancel2 := initContexts(c.timeout) + if cancel != nil && cancel2 != nil { + defer cancel() + defer cancel2() + } + sshSettings := sshtesting.CreateDefaultTestSettings(test) + goSSHClient, err := initBothClients(t, ctx, sshSettings, sess, keys) + require.NoError(t, err) + + var gocmd, clicmd pkg.Command + gocmd = gossh.NewSSHCommand(goSSHClient.(*gossh.Client), c.command, c.args...) + clicmd = clissh.NewCommand(sshSettings, sess, c.command, c.args...) + + goout, _, err := gocmd.Output(ctx) + cliout, _, err2 := clicmd.Output(ctx2) + if !c.wantErr { + require.NoError(t, err) + require.NoError(t, err2) + require.Equal(t, c.expectedOutput, string(goout)) + require.Equal(t, c.expectedOutput, string(cliout)) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + } + }) + } + }) +} + +func TestCommandCombinedOutput(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestCommandCombinedOutput") + + container := sshtesting.NewTestContainerWrapper(t, test) + sess := sshtesting.Session(container) + keys := container.AgentPrivateKeys() + + t.Run("Get command CombinedOutput", func(t *testing.T) { + cases := []struct { + title string + command string + args []string + expectedOutput string + expectedErrOutput string + timeout time.Duration + wantErr bool + err string + }{ + { + title: "Just echo, success", + command: "echo", + args: []string{"\"test output\""}, + expectedOutput: "test output\n", + wantErr: false, + }, + { + title: "With context", + command: "while true; do echo \"test\"; sleep 5; done", + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", + }, + { + title: "Command return error", + command: "cat", + args: []string{"\"/etc/sudoers\""}, + wantErr: true, + err: "status 1", + expectedErrOutput: "cat: /etc/sudoers: Permission denied\n", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + ctx, ctx2, cancel, cancel2 := initContexts(c.timeout) + if cancel != nil && cancel2 != nil { + defer cancel() + defer cancel2() + } + sshSettings := sshtesting.CreateTestSettingNoDebug(test) + goSSHClient, err := initBothClients(t, ctx, sshSettings, sess, keys) + require.NoError(t, err) + + var gocmd, clicmd pkg.Command + gocmd = gossh.NewSSHCommand(goSSHClient.(*gossh.Client), c.command, c.args...) + clicmd = clissh.NewCommand(sshSettings, sess, c.command, c.args...) + gocombined, err := gocmd.CombinedOutput(ctx) + clicombined, err2 := clicmd.CombinedOutput(ctx2) + if !c.wantErr { + require.NoError(t, err) + require.NoError(t, err2) + require.Contains(t, string(gocombined), c.expectedOutput) + require.Contains(t, string(clicombined), c.expectedOutput) + } else { + require.Error(t, err) + require.Contains(t, string(gocombined), c.expectedErrOutput) + require.Contains(t, string(clicombined), c.expectedErrOutput) + require.Contains(t, err.Error(), c.err) + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + } + }) + } + }) +} + +func TestCommandRun(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestCommandRun") + + container := sshtesting.NewTestContainerWrapper(t, test) + sess := sshtesting.Session(container) + keys := container.AgentPrivateKeys() + + // evns test + envs := make(map[string]string) + envs["TEST_ENV"] = "test" + + t.Run("Run a command", func(t *testing.T) { + cases := []struct { + title string + command string + args []string + expectedOutput string + expectedErrOutput string + timeout time.Duration + prepareFunc func(c pkg.Command) error + envs map[string]string + wantErr bool + err string + }{ + { + title: "Just echo, success", + command: "echo", + args: []string{"\"test output\""}, + expectedOutput: "test output\n", + wantErr: false, + }, + { + title: "Just echo, with envs, success", + command: "echo", + args: []string{"\"test output\""}, + expectedOutput: "test output\n", + envs: envs, + wantErr: false, + }, + { + title: "With context", + command: "while true; do echo \"test\"; sleep 5; done", + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", + expectedErrOutput: "test\ntest\n", + }, + { + title: "Command return error", + command: "cat", + args: []string{`"/etc/sudoers"`}, + wantErr: true, + err: "status 1", + expectedErrOutput: "cat: /etc/sudoers: Permission denied\n", + }, + { + title: "With opened stdout pipe", + command: "echo", + args: []string{"\"test output\""}, + prepareFunc: func(c pkg.Command) error { + return c.Run(context.Background()) + }, + wantErr: true, + err: "already started", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + var emptyDuration time.Duration + ctx, ctx2, cancel, cancel2 := initContexts(c.timeout) + if cancel != nil && cancel2 != nil { + defer cancel() + defer cancel2() + } + sshSettings := sshtesting.CreateDefaultTestSettings(test) + goSSHClient, err := initBothClients(t, ctx, sshSettings, sess, keys) + require.NoError(t, err) + + var gocmd, clicmd pkg.Command + gocmd = gossh.NewSSHCommand(goSSHClient.(*gossh.Client), c.command, c.args...) + clicmd = clissh.NewCommand(sshSettings, sess, c.command, c.args...) + clicmd.Cmd(ctx2) + if c.prepareFunc != nil { + err = c.prepareFunc(gocmd) + require.NoError(t, err) + err = c.prepareFunc(clicmd) + require.NoError(t, err) + } + if len(c.envs) > 0 { + gocmd.WithEnv(c.envs) + clicmd.WithEnv(c.envs) + } + + err = gocmd.Run(ctx) + err2 := clicmd.Run(ctx2) + if !c.wantErr { + require.NoError(t, err) + require.NoError(t, err2) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + } + + // second run for context after deadline exceeded + if c.timeout != emptyDuration { + gocmd2 := gossh.NewSSHCommand(goSSHClient.(*gossh.Client), c.command, c.args...) + clicmd2 := clissh.NewCommand(sshSettings, sess, c.command, c.args...) + clicmd2.Cmd(ctx2) + if c.prepareFunc != nil { + err = c.prepareFunc(gocmd2) + require.NoError(t, err) + err = c.prepareFunc(clicmd2) + require.NoError(t, err) + } + if len(c.envs) > 0 { + gocmd2.WithEnv(c.envs) + clicmd2.WithEnv(c.envs) + } + err = gocmd2.Run(ctx) + err2 = clicmd2.Run(ctx2) + // command should fail to run + require.Error(t, err) + require.Contains(t, err.Error(), "context deadline exceeded") + require.Error(t, err2) + require.Contains(t, err2.Error(), "context deadline exceeded") + } + }) + } + }) +} + +func TestCommandSudoRun(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestCommandRunSudo") + + container := sshtesting.NewTestContainerWrapper(t, test, sshtesting.WithNoPassword()) + keys := container.AgentPrivateKeys() + + // starting openssh container with password auth + containerWithPass := sshtesting.NewTestContainerWrapper( + t, + test, + sshtesting.WithPassword(sshtesting.RandPassword(12)), + sshtesting.WithConnectToContainerNetwork(container), + ) + keysContainerWithPass := containerWithPass.AgentPrivateKeys() + + sessionWithoutPassword := sshtesting.Session(container) + + sessionWithValidPass := sshtesting.Session(containerWithPass) + + // client with wrong sudo password + sessionWithInvalidPass := sshtesting.Session(containerWithPass, func(input *session.Input) { + input.BecomePass = sshtesting.RandPassword(3) + }) + + t.Run("Run a command with sudo", func(t *testing.T) { + cases := []struct { + title string + settings *session.Session + keys []session.AgentPrivateKey + command string + args []string + timeout time.Duration + wantErr bool + err string + errorOutput string + }{ + { + title: "Just echo, success", + settings: sessionWithoutPassword, + keys: keys, + command: "echo", + args: []string{`"test output"`}, + wantErr: false, + }, + { + title: "Just echo, success, with password", + settings: sessionWithValidPass, + keys: keysContainerWithPass, + command: "echo", + args: []string{`"test output"`}, + wantErr: false, + }, + { + title: "Just echo, failure, with wrong password", + settings: sessionWithInvalidPass, + keys: keysContainerWithPass, + command: "echo", + args: []string{`"test output"`}, + wantErr: true, + err: "status 1", + errorOutput: "SudoPasswordSorry, try again.\nSudoPasswordSorry, try again.\nSudoPasswordsudo: 3 incorrect password attempts\n", + }, + { + title: "With context", + settings: sessionWithoutPassword, + keys: keys, + command: `while true; do echo "test"; sleep 5; done`, + args: []string{}, + timeout: 7 * time.Second, + wantErr: true, + err: "context deadline exceeded", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + ctx, ctx2, cancel, cancel2 := initContexts(c.timeout) + if cancel != nil && cancel2 != nil { + defer cancel() + defer cancel2() + } + sshSettings := sshtesting.CreateDefaultTestSettings(test) + goSSHClient, err := initBothClients(t, ctx, sshSettings, c.settings, c.keys) + require.NoError(t, err) + + var gocmd, clicmd pkg.Command + gocmd = gossh.NewSSHCommand(goSSHClient.(*gossh.Client), c.command, c.args...) + clicmd = clissh.NewCommand(sshSettings, c.settings, c.command, c.args...) + clicmd.Cmd(ctx2) + + gocmd.Sudo(ctx) + clicmd.Sudo(ctx) + err = gocmd.Run(ctx) + err2 := clicmd.Run(ctx2) + if !c.wantErr { + require.NoError(t, err) + require.NoError(t, err2) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + errBytes := gocmd.StderrBytes() + require.Contains(t, string(errBytes), c.errorOutput) + + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + } + }) + } + }) +} diff --git a/pkg/ssh/testssh/common_test.go b/pkg/ssh/testssh/common_test.go new file mode 100644 index 0000000..234233c --- /dev/null +++ b/pkg/ssh/testssh/common_test.go @@ -0,0 +1,164 @@ +// Copyright 2025 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 testssh + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/deckhouse/lib-dhctl/pkg/retry" + "github.com/stretchr/testify/require" + + "github.com/deckhouse/lib-connection/pkg" + "github.com/deckhouse/lib-connection/pkg/settings" + "github.com/deckhouse/lib-connection/pkg/ssh/clissh" + "github.com/deckhouse/lib-connection/pkg/ssh/gossh" + sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" + "github.com/deckhouse/lib-connection/pkg/ssh/session" +) + +const expectedFileContent = "Some test data" + +func registerStopClient(t *testing.T, sshClient pkg.SSHClient) { + t.Cleanup(func() { + sshClient.Stop() + }) +} + +func newSessionTestLoopParams() gossh.ClientLoopsParams { + return gossh.ClientLoopsParams{ + NewSession: retry.NewEmptyParams( + retry.WithWait(2*time.Second), + retry.WithAttempts(5), + ), + } +} + +func initBothClients(t *testing.T, ctx context.Context, setting settings.Settings, sess *session.Session, keys []session.AgentPrivateKey) (pkg.SSHClient, error) { + goSSHClient := gossh.NewClient(ctx, setting, sess, keys). + WithLoopsParams(newSessionTestLoopParams()) + err := goSSHClient.Start() + if err != nil { + return nil, err + } + registerStopClient(t, goSSHClient) + cliSSHClient := clissh.NewClient(setting, sess, keys, true) + err = cliSSHClient.Start() + + return goSSHClient, err +} + +func initContexts(dur time.Duration) (context.Context, context.Context, context.CancelFunc, context.CancelFunc) { + ctx := context.Background() + ctx2 := context.Background() + var emptyDuration time.Duration + var cancel, cancel2 context.CancelFunc + if dur != emptyDuration { + ctx, cancel = context.WithDeadline(ctx, time.Now().Add(dur)) + ctx2, cancel2 = context.WithDeadline(ctx, time.Now().Add(dur)) + } + return ctx, ctx2, cancel, cancel2 +} + +// todo mount local directory to container and assert via local exec +func assertFilesViaRemoteRun(t *testing.T, sshClient *gossh.Client, cmd string, expectedOutput string) { + s, err := sshClient.NewSSHSession() + require.NoError(t, err, "session should start") + defer sshClient.UnregisterSession(s) + out, err := s.Output(cmd) + require.NoError(t, err) + // out contains a contant of uploaded file, should be equal to testFile contant + require.Equal(t, expectedOutput, string(out)) +} + +func startTwoContainersWithClients(t *testing.T, test *sshtesting.Test, createDeckhouseDirs bool) (pkg.SSHClient, pkg.SSHClient, pkg.SSHClient, error) { + // first container for gossh client + container := sshtesting.NewTestContainerWrapper(t, test) + ctx := context.Background() + sess := sshtesting.Session(container) + keys := container.AgentPrivateKeys() + sshSettings := sshtesting.CreateTestSettingNoDebug(test) + goSSHClient := gossh.NewClient(ctx, sshSettings, sess, keys). + WithLoopsParams(newSessionTestLoopParams()) + err := goSSHClient.Start() + if err != nil { + return nil, nil, nil, err + } + registerStopClient(t, goSSHClient) + + // second container for clissh + container2 := sshtesting.NewTestContainerWrapper(t, test, sshtesting.WithConnectToContainerNetwork(container)) + sess2 := sshtesting.Session(container2) + keys2 := container2.AgentPrivateKeys() + + // check connection + goSSHClient2 := gossh.NewClient(ctx, sshSettings, sess2, keys2). + WithLoopsParams(newSessionTestLoopParams()) + err = goSSHClient2.Start() + if err != nil { + return nil, nil, nil, err + } + goSSHClient2.Stop() + + if createDeckhouseDirs { + err = container.Container.CreateDeckhouseDirs() + if err != nil { + return nil, nil, nil, err + } + err = container2.Container.CreateDeckhouseDirs() + if err != nil { + return nil, nil, nil, err + } + } + + cliSSHClient := clissh.NewClient(sshSettings, sess2, keys2, true) + err = cliSSHClient.Start() + + return goSSHClient, cliSSHClient, goSSHClient2, err +} + +func prepareScp(t *testing.T) { + path := filepath.Join(os.Getenv("PWD"), "bin") + err := os.MkdirAll(path, 0o777) + require.NoError(t, err) + err = os.Symlink("/usr/bin/ssh", filepath.Join(path, "ssh")) + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(path) + }) +} + +func mustPrepareData(t *testing.T, sshClient pkg.SSHClient) { + err := sshClient.Command("mkdir -p /tmp/testdata").Run(context.Background()) + require.NoError(t, err) + err = sshClient.Command(fmt.Sprintf(`echo -n '%s' > /tmp/testdata/first`, expectedFileContent)).Run(context.Background()) + require.NoError(t, err) + err = sshClient.Command("touch /tmp/testdata/second").Run(context.Background()) + require.NoError(t, err) + err = sshClient.Command("touch /tmp/testdata/third").Run(context.Background()) + require.NoError(t, err) + err = sshClient.Command("ln -s /tmp/testdata/first /tmp/link").Run(context.Background()) + require.NoError(t, err) +} + +func chmodTmpDir(sshClient pkg.SSHClient, nodeTmpPath string) error { + cmd := sshClient.Command("chmod", "700", nodeTmpPath) + cmd.Sudo(context.Background()) + return cmd.Run(context.Background()) +} diff --git a/pkg/ssh/testssh/file_test.go b/pkg/ssh/testssh/file_test.go new file mode 100644 index 0000000..fc09055 --- /dev/null +++ b/pkg/ssh/testssh/file_test.go @@ -0,0 +1,489 @@ +// Copyright 2025 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 testssh + +import ( + "context" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/deckhouse/lib-connection/pkg/ssh/gossh" + sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" +) + +func TestFileUpload(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestCommandOutput") + + const uploadDir = "upload_dir" + const testFileContent = "Hello World" + const notExec = false + + filePath := func(subPath ...string) []string { + require.NotEmpty(t, subPath, "subPath is empty for filePath") + return append([]string{uploadDir}, subPath...) + } + + testFile := test.MustCreateTmpFile(t, testFileContent, notExec, filePath("upload")...) + testDir := filepath.Dir(testFile) + test.MustCreateTmpFile(t, "second", notExec, filePath("second")...) + test.MustCreateTmpFile(t, "empty", notExec, filePath("second")...) + test.MustCreateTmpFile(t, "sub", notExec, filePath("sub", "third")...) + + symlink := filepath.Join(test.TmpDir(), "symlink") + err := os.Symlink(testFile, symlink) + require.NoError(t, err) + + const unaccessibleDirectoryName = "unaccessible" + test.MustCreateUnaccessibleDir(t, unaccessibleDirectoryName) + unaccessibleDirectoryPath := filepath.Join(test.TmpDir(), unaccessibleDirectoryName) + + goSSHClient, cliSSHClient, goSSHClient2, err := startTwoContainersWithClients(t, test, false) + require.NoError(t, err) + + prepareScp(t) + + t.Run("Upload files and directories to container via existing ssh client", func(t *testing.T) { + cases := []struct { + title string + srcPath string + dstPath string + wantErr bool + err string + }{ + { + title: "Single file", + srcPath: testFile, + dstPath: ".", + wantErr: false, + }, + { + title: "Directory", + srcPath: testDir, + dstPath: "/tmp", + wantErr: false, + }, + { + title: "Nonexistent", + srcPath: "/path/to/nonexistent/flie", + dstPath: "/tmp", + wantErr: true, + }, + { + title: "File to root", + srcPath: testFile, + dstPath: "/any", + wantErr: true, + }, + { + title: "File to /var/lib", + srcPath: testFile, + dstPath: "/var/lib", + wantErr: true, + }, + { + title: "File to unaccessible file", + srcPath: testFile, + dstPath: "/path/what/not/exists.txt", + wantErr: true, + }, + { + title: "Directory to root", + srcPath: testDir, + dstPath: "/", + wantErr: true, + }, + { + title: "Symlink", + srcPath: symlink, + dstPath: ".", + wantErr: false, + }, + { + title: "Device", + srcPath: "/dev/zero", + dstPath: "/", + wantErr: true, + err: "is not a directory or file", + }, + { + title: "Unaccessible dir", + srcPath: unaccessibleDirectoryPath, + dstPath: ".", + wantErr: true, + }, + { + title: "Unaccessible file", + srcPath: "/etc/sudoers", + dstPath: ".", + wantErr: true, + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + f := goSSHClient.File() + f2 := cliSSHClient.File() + err = f.Upload(context.Background(), c.srcPath, c.dstPath) + err2 := f2.Upload(context.Background(), c.srcPath, c.dstPath) + if !c.wantErr { + require.NoError(t, err) + require.NoError(t, err2) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + } + }) + } + }) + + t.Run("Equality of uploaded and local file content", func(t *testing.T) { + f := goSSHClient.File() + err := f.Upload(context.Background(), testFile, "/tmp/testfile.txt") + // testFile contains "Hello world" string + require.NoError(t, err) + + assertFilesViaRemoteRun(t, goSSHClient.(*gossh.Client), "cat /tmp/testfile.txt", testFileContent) + + // clissh check + f = cliSSHClient.File() + err = f.Upload(context.Background(), testFile, "/tmp/testfile.txt") + require.NoError(t, err) + + err = goSSHClient2.Start() + require.NoError(t, err) + registerStopClient(t, goSSHClient2) + + assertFilesViaRemoteRun(t, goSSHClient2.(*gossh.Client), "cat /tmp/testfile.txt", testFileContent) + }) + + t.Run("Equality of uploaded and local directory", func(t *testing.T) { + f := goSSHClient.File() + err := f.Upload(context.Background(), testDir, "/tmp/upload") + require.NoError(t, err) + + cmd := exec.Command("ls", testDir) + lsResult, err := cmd.Output() + require.NoError(t, err) + + assertFilesViaRemoteRun(t, goSSHClient.(*gossh.Client), "ls /tmp/upload", string(lsResult)) + + // clissh + f = cliSSHClient.File() + err = f.Upload(context.Background(), testDir, "/tmp/upload") + require.NoError(t, err) + + err = goSSHClient2.Start() + require.NoError(t, err) + registerStopClient(t, goSSHClient2) + + assertFilesViaRemoteRun(t, goSSHClient2.(*gossh.Client), "ls /tmp/upload", string(lsResult)) + }) +} + +func TestFileUploadBytes(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestSSHFileUploadBytes") + + goSSHClient, cliSSHClient, goSSHClient2, err := startTwoContainersWithClients(t, test, false) + require.NoError(t, err) + + prepareScp(t) + err = os.MkdirAll(goSSHClient.(*gossh.Client).Settings().TmpDir(), 0o777) + require.NoError(t, err) + + t.Run("Upload bytes", func(t *testing.T) { + const content = "Hello world" + f := goSSHClient.File() + err := f.UploadBytes(context.Background(), []byte(content), "/tmp/testfile.txt") + require.NoError(t, err) + + assertFilesViaRemoteRun(t, goSSHClient.(*gossh.Client), "cat /tmp/testfile.txt", content) + + // clissh + f = cliSSHClient.File() + err = f.UploadBytes(context.Background(), []byte(content), "/tmp/testfile.txt") + require.NoError(t, err) + + err = goSSHClient2.Start() + require.NoError(t, err) + registerStopClient(t, goSSHClient2) + + assertFilesViaRemoteRun(t, goSSHClient2.(*gossh.Client), "cat /tmp/testfile.txt", content) + }) +} + +func TestFileDownload(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestSSHFileDownload") + + goSSHClient, cliSSHClient, goSSHClient2, err := startTwoContainersWithClients(t, test, false) + require.NoError(t, err) + + prepareScp(t) + + // preparing some test related data + mustPrepareData(t, goSSHClient) + mustPrepareData(t, cliSSHClient) + + t.Run("Download files and directories to container via existing ssh client", func(t *testing.T) { + testDir := test.MustMkSubDirs(t, "download") + + cases := []struct { + title string + srcPath string + dstPath string + wantErr bool + err string + }{ + { + title: "Single file", + srcPath: "/tmp/testdata/first", + dstPath: testDir, + wantErr: false, + }, + { + title: "Directory", + srcPath: "/tmp/testdata", + dstPath: filepath.Join(testDir, "downloaded"), + wantErr: false, + }, + { + title: "Nonexistent", + srcPath: "/path/to/nonexistent/file", + dstPath: "/tmp", + wantErr: true, + }, + { + title: "File to root", + srcPath: "/tmp/testdata/first", + dstPath: "/any", + wantErr: true, + }, + { + title: "File to /var/lib", + srcPath: "/tmp/testdata/first", + dstPath: "/var/lib", + wantErr: true, + }, + { + title: "File to unaccessible file", + srcPath: "/tmp/testdata/first", + dstPath: "/path/what/not/exists.txt", + wantErr: true, + err: "no such file or directory", + }, + { + title: "Directory to root", + srcPath: "/tmp/testdata", + dstPath: "/", + wantErr: true, + }, + { + title: "Symlink", + srcPath: "/tmp/link", + dstPath: testDir, + wantErr: false, + }, + { + title: "Device", + srcPath: "/dev/zero", + dstPath: "/", + wantErr: true, + err: "failed to open local file", + }, + { + title: "Unaccessible dir", + srcPath: "/var/audit", + dstPath: testDir, + wantErr: true, + }, + { + title: "Unaccessible file", + srcPath: "/etc/sudoers", + dstPath: testDir, + wantErr: true, + err: "failed to copy file from remote host", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + // cleanup test directory to make sure previous run cannot affect current run + os.RemoveAll(testDir) + testDir = test.MustMkSubDirs(t, "download") + // do test + f := goSSHClient.File() + err = f.Download(context.Background(), c.srcPath, c.dstPath) + if c.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + return + } + + require.NoError(t, err) + + _, err = os.Stat(c.dstPath) + require.NoError(t, err, "%s path should exist after download", c.dstPath) + + // cleanup and download via clissh, then do the check again + err = os.RemoveAll(c.dstPath) + require.NoError(t, err) + f = cliSSHClient.File() + err = f.Download(context.Background(), c.srcPath, c.dstPath) + if c.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + return + } + + require.NoError(t, err) + + _, err = os.Stat(c.dstPath) + require.NoError(t, err, "%s path should exist after download", c.dstPath) + }) + } + }) + + t.Run("Equality of downloaded and remote file content", func(t *testing.T) { + downloadContentDir := test.MustMkSubDirs(t, "download_content") + + f := goSSHClient.File() + + dstPath := path.Join(downloadContentDir, "testfile.txt") + + err := f.Download(context.Background(), "/tmp/testdata/first", dstPath) + // /tmp/testdata/first contains "Some test data" string + require.NoError(t, err) + downloadedContent, err := os.ReadFile(dstPath) + require.NoError(t, err) + + assertFilesViaRemoteRun(t, goSSHClient.(*gossh.Client), "cat /tmp/testdata/first", string(downloadedContent)) + + // out contains a contant of uploaded file, should be equal to testFile contant + require.Equal(t, expectedFileContent, string(downloadedContent)) + + // cleanup and download via clissh, then do the check again + err = os.Remove(dstPath) + require.NoError(t, err) + + f = cliSSHClient.File() + err = f.Download(context.Background(), "/tmp/testdata/first", dstPath) + // /tmp/testdata/first contains "Some test data" string + require.NoError(t, err) + downloadedContent, err = os.ReadFile(dstPath) + require.NoError(t, err) + + err = goSSHClient2.Start() + require.NoError(t, err) + registerStopClient(t, goSSHClient2) + + assertFilesViaRemoteRun(t, goSSHClient2.(*gossh.Client), "cat /tmp/testdata/first", string(downloadedContent)) + require.Equal(t, expectedFileContent, string(downloadedContent)) + }) + + t.Run("Equality of downloaded and remote directory", func(t *testing.T) { + downloadWholeDirDir := test.MustMkSubDirs(t, "download_dir") + + f := goSSHClient.File() + err = f.Download(context.Background(), "/tmp/testdata", downloadWholeDirDir) + require.NoError(t, err) + + cmd := exec.Command("ls", filepath.Join(downloadWholeDirDir, "testdata")) + lsResult, err := cmd.Output() + require.NoError(t, err) + + assertFilesViaRemoteRun(t, goSSHClient.(*gossh.Client), "ls /tmp/testdata/", string(lsResult)) + + // cleanup and download via clissh, then do the check again + err = os.RemoveAll(downloadWholeDirDir) + require.NoError(t, err) + + f = cliSSHClient.File() + err = f.Download(context.Background(), "/tmp/testdata", downloadWholeDirDir) + require.NoError(t, err) + + cmd = exec.Command("ls", downloadWholeDirDir) + lsResult, err = cmd.CombinedOutput() + test.Logger.InfoF(string(lsResult)) + require.NoError(t, err) + + err = goSSHClient2.Start() + require.NoError(t, err) + registerStopClient(t, goSSHClient2) + + assertFilesViaRemoteRun(t, goSSHClient2.(*gossh.Client), "ls /tmp/testdata/", string(lsResult)) + }) +} + +func TestFileDownloadBytes(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestSSHFileDownloadBytes") + + goSSHClient, cliSSHClient, _, err := startTwoContainersWithClients(t, test, false) + require.NoError(t, err) + + prepareScp(t) + + const expectedFileContent = "Some test data" + + // preparing file to download + err = goSSHClient.Command(fmt.Sprintf(`echo -n '%s' > /tmp/testfile`, expectedFileContent)).Run(context.Background()) + require.NoError(t, err) + err = cliSSHClient.Command(fmt.Sprintf(`echo -n '%s' > /tmp/testfile`, expectedFileContent)).Run(context.Background()) + require.NoError(t, err) + + t.Run("Download bytes", func(t *testing.T) { + cases := []struct { + title string + remotePath string + wantErr bool + }{ + { + title: "Positive result", + remotePath: "/tmp/testfile", + wantErr: false, + }, + { + title: "Unaccessible remote file", + remotePath: "/etc/sudoers", + wantErr: true, + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + f := goSSHClient.File() + bytes, err := f.DownloadBytes(context.Background(), c.remotePath) + f2 := cliSSHClient.File() + bytes2, err2 := f2.DownloadBytes(context.Background(), c.remotePath) + if c.wantErr { + require.Error(t, err) + require.Error(t, err2) + } else { + require.NoError(t, err) + require.NoError(t, err2) + // out contains a contant of uploaded file, should be equal to testFile contant + require.Equal(t, expectedFileContent, string(bytes)) + require.Equal(t, expectedFileContent, string(bytes2)) + } + }) + } + }) +} diff --git a/pkg/ssh/testssh/upload-script_test.go b/pkg/ssh/testssh/upload-script_test.go new file mode 100644 index 0000000..d7899f3 --- /dev/null +++ b/pkg/ssh/testssh/upload-script_test.go @@ -0,0 +1,221 @@ +// Copyright 2025 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 testssh + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/deckhouse/lib-connection/pkg" + sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" +) + +func TestUploadScriptExecute(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestUploadScriptExecute") + + goSSHClient, cliSSHClient, _, err := startTwoContainersWithClients(t, test, true) + require.NoError(t, err) + prepareScp(t) + + script := `#!/bin/bash +if [[ $# -eq 0 ]]; then + echo "Error: No arguments provided." + exit 1 +elif [[ $# -gt 1 ]]; then + echo "Usage: $0 " + exit 1 +else + echo "provided: $1" +fi +` + scriptFile := test.MustCreateTmpFile(t, script, true, "execute_script", "script.sh") + + // evns test + envs := map[string]string{ + "TEST_ENV": "test", + } + + t.Run("Upload and execute script to container via existing ssh client", func(t *testing.T) { + cases := []struct { + title string + scriptPath string + scriptArgs []string + expected string + wantSudo bool + envs map[string]string + wantErr bool + err string + }{ + { + title: "Happy case", + scriptPath: scriptFile, + scriptArgs: []string{"one"}, + expected: "provided: one", + wantSudo: false, + wantErr: false, + }, + { + title: "Happy case with sudo", + scriptPath: scriptFile, + scriptArgs: []string{"one"}, + expected: "provided: one", + wantSudo: true, + wantErr: false, + }, + { + title: "Error by remote script execution", + scriptPath: scriptFile, + scriptArgs: []string{"one", "two"}, + wantSudo: false, + wantErr: true, + err: "execute on remote", + }, + { + title: "With envs", + scriptPath: scriptFile, + scriptArgs: []string{"one"}, + expected: "provided: one", + wantSudo: false, + envs: envs, + wantErr: false, + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + var s, s2 pkg.Script + s = goSSHClient.UploadScript(c.scriptPath, c.scriptArgs...) + s.WithCleanupAfterExec(true) + + s2 = cliSSHClient.UploadScript(c.scriptPath, c.scriptArgs...) + s2.WithCleanupAfterExec(true) + + if c.wantSudo { + s.Sudo() + s2.Sudo() + } + if len(c.envs) > 0 { + s.WithEnvs(c.envs) + s2.WithEnvs(c.envs) + } + + out, err := s.Execute(context.Background()) + out2, err2 := s2.Execute(context.Background()) + if c.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + return + } + + require.NoError(t, err) + require.Contains(t, string(out), c.expected) + require.NoError(t, err2) + require.Contains(t, string(out2), c.expected) + }) + } + }) +} + +func TestUploadScriptExecuteBundle(t *testing.T) { + test := sshtesting.ShouldNewTest(t, "TestUploadScriptExecuteBundle") + + goSSHClient, cliSSHClient, _, err := startTwoContainersWithClients(t, test, true) + require.NoError(t, err) + prepareScp(t) + + const ( + entrypoint = "test.sh" + nodeTmpPath = "/opt/deckhouse/tmp" + ) + + testDir := sshtesting.PrepareFakeBashibleBundle(t, test, entrypoint, "bashible") + + t.Run("Upload and execute bundle to container via existing ssh client", func(t *testing.T) { + cases := []struct { + title string + scriptArgs []string + parentDir string + bundleDir string + prepareFunc func() error + wantErr bool + err string + }{ + { + title: "Happy case", + scriptArgs: []string{}, + parentDir: testDir, + bundleDir: "bashible", + wantErr: false, + }, + { + title: "Bundle error", + scriptArgs: []string{"--add-failure"}, + parentDir: testDir, + bundleDir: "bashible", + wantErr: true, + }, + { + title: "Wrong bundle directory", + scriptArgs: []string{}, + parentDir: "/path/to/nonexistent/dir", + bundleDir: "wrong_bundle", + wantErr: true, + err: "tar bundle: failed to walk path", + }, + { + title: "Upload error", + scriptArgs: []string{""}, + parentDir: testDir, + bundleDir: "bashible", + prepareFunc: func() error { + err := chmodTmpDir(goSSHClient, nodeTmpPath) + if err != nil { + return err + } + return chmodTmpDir(cliSSHClient, nodeTmpPath) + }, + wantErr: true, + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + s := goSSHClient.UploadScript(entrypoint, c.scriptArgs...) + s2 := cliSSHClient.UploadScript(entrypoint, c.scriptArgs...) + if c.prepareFunc != nil { + err = c.prepareFunc() + require.NoError(t, err) + } + + _, err := s.ExecuteBundle(context.Background(), c.parentDir, c.bundleDir) + _, err2 := s2.ExecuteBundle(context.Background(), c.parentDir, c.bundleDir) + if c.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), c.err) + require.Error(t, err2) + require.Contains(t, err2.Error(), c.err) + return + } + + require.NoError(t, err) + require.NoError(t, err2) + }) + } + }) +} diff --git a/pkg/ssh/utils/checks_test.go b/pkg/ssh/utils/checks_test.go index b862018..b2ea960 100644 --- a/pkg/ssh/utils/checks_test.go +++ b/pkg/ssh/utils/checks_test.go @@ -145,7 +145,6 @@ func TestSSHHostChecks(t *testing.T) { "master-0": passedHosts[0].Host, }, "nodes name must be the same") }) - }) }) } diff --git a/pkg/ssh/utils/execute_path_test.go b/pkg/ssh/utils/execute_path_test.go index 3cc4049..2cd6f09 100644 --- a/pkg/ssh/utils/execute_path_test.go +++ b/pkg/ssh/utils/execute_path_test.go @@ -17,9 +17,10 @@ package utils import ( "testing" + "github.com/stretchr/testify/require" + "github.com/deckhouse/lib-connection/pkg/settings" sshtesting "github.com/deckhouse/lib-connection/pkg/ssh/gossh/testing" - "github.com/stretchr/testify/require" ) type testScriptPath struct { @@ -110,7 +111,7 @@ func TestExecuteRemoteScriptPath(t *testing.T) { for _, tst := range tests { t.Run(tst.name, func(t *testing.T) { - sshSettings := sshtesting.CreateDefaultTestSettings(sshtesting.ShouldNewTest(t, "")) + sshSettings := sshtesting.CreateDefaultTestSettings(sshtesting.ShouldNewTest(t, tst.name)) script := &testScriptPath{ sudo: tst.sudo, uploadDir: tst.uploadDir, diff --git a/pkg/ssh/utils/matcher.go b/pkg/ssh/utils/matcher.go index 0503857..bd2e1d9 100644 --- a/pkg/ssh/utils/matcher.go +++ b/pkg/ssh/utils/matcher.go @@ -51,7 +51,7 @@ func (m *ByteSequenceMatcher) WaitNonMatched() *ByteSequenceMatcher { // when match is found, return 0 // return index (0 or more) of a first byte after pattern and \r, \n // This behaviour is used to write bytes to Reader only after match is found. -func (m *ByteSequenceMatcher) Analyze(buf []byte) (n int) { +func (m *ByteSequenceMatcher) Analyze(buf []byte) int { for i, b := range buf { // ignore \r and \n if b == '\r' || b == '\n' { diff --git a/pkg/ssh/utils/privatekeys.go b/pkg/ssh/utils/privatekeys.go index 7b9dde2..d922e50 100644 --- a/pkg/ssh/utils/privatekeys.go +++ b/pkg/ssh/utils/privatekeys.go @@ -73,13 +73,20 @@ func (c *DefaultPassphraseOnlyConsumer) AskPassword(prompt string) ([]byte, erro return nil, fmt.Errorf("%s. AskPassword not allow for DefaultPassphraseOnlyConsumer", prompt) } -func ParseSSHPrivateKeyFile(path string, logger log.Logger) (any, string, error) { +func ParseSSHPrivateKeyFile(path string, password string, logger log.Logger) (any, string, error) { content, err := os.ReadFile(path) if err != nil { - return nil, "", fmt.Errorf("cannot read private key file %s: %w", path, err) + return nil, "", fmt.Errorf("Cannot read private key file %s: %w", path, err) } - return ParseSSHPrivateKey(content, path, NewTerminalPassphraseConsumer(logger, make([]byte, 0))) + return ParseSSHPrivateKey( + content, + path, + NewTerminalPassphraseConsumer( + logger, + []byte(password), + ), + ) } func ParseSSHPrivateKey(keyData []byte, keyName string, passphraseConsumer PassphraseConsumer) (any, string, error) { diff --git a/pkg/ssh/utils/terminal/ask_password.go b/pkg/ssh/utils/terminal/ask_password.go index f8526ad..e6f910c 100644 --- a/pkg/ssh/utils/terminal/ask_password.go +++ b/pkg/ssh/utils/terminal/ask_password.go @@ -31,7 +31,7 @@ func AskPassword(logger log.Logger, prompt string) ([]byte, error) { logger.InfoF(prompt) data, err := terminal.ReadPassword(fd) - logger.InfoLn() + logger.InfoF("") if err != nil { return nil, fmt.Errorf("read secret: %w", err) diff --git a/pkg/ssh/utils/waiting.go b/pkg/ssh/utils/waiting.go index 8849113..a1a402f 100644 --- a/pkg/ssh/utils/waiting.go +++ b/pkg/ssh/utils/waiting.go @@ -22,10 +22,11 @@ import ( "strings" "time" + "github.com/deckhouse/lib-dhctl/pkg/retry" + ssh "github.com/deckhouse/lib-connection/pkg" "github.com/deckhouse/lib-connection/pkg/settings" "github.com/deckhouse/lib-connection/pkg/ssh/session" - "github.com/deckhouse/lib-dhctl/pkg/retry" ) var defaultAvailabilityOpts = []retry.ParamsBuilderOpt{ diff --git a/pkg/ssh/wrapper.go b/pkg/ssh/wrapper.go index cbf482a..c48b45e 100644 --- a/pkg/ssh/wrapper.go +++ b/pkg/ssh/wrapper.go @@ -41,8 +41,8 @@ func NewNodeInterfaceWrapper(sshClient connection.SSHClient, sett settings.Setti func (n *NodeInterfaceWrapper) Command(name string, args ...string) connection.Command { logger := n.settings.Logger() - logger.DebugLn("Starting NodeInterfaceWrapper.command") - defer logger.DebugLn("Stop NodeInterfaceWrapper.command") + logger.DebugF("Starting NodeInterfaceWrapper.command") + defer logger.DebugF("Stop NodeInterfaceWrapper.command") return n.sshClient.Command(name, args...) } @@ -54,8 +54,8 @@ func (n *NodeInterfaceWrapper) File() connection.File { func (n *NodeInterfaceWrapper) UploadScript(scriptPath string, args ...string) connection.Script { logger := n.settings.Logger() - logger.DebugLn("Starting NodeInterfaceWrapper.UploadScript") - defer logger.DebugLn("Stop NodeInterfaceWrapper.UploadScript") + logger.DebugF("Starting NodeInterfaceWrapper.UploadScript") + defer logger.DebugF("Stop NodeInterfaceWrapper.UploadScript") return n.sshClient.UploadScript(scriptPath, args...) } diff --git a/tests/go.mod b/tests/go.mod index 5ee6068..9943aeb 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -4,6 +4,48 @@ go 1.25.5 require github.com/deckhouse/lib-connection v0.0.0-00010101000000-000000000000 +require ( + github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect + github.com/deckhouse/deckhouse/pkg/log v0.1.1-0.20251230144142-2bad7c3d1edf // indirect + github.com/deckhouse/lib-dhctl v0.11.0 // indirect + github.com/deckhouse/lib-gossh v0.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-openapi/analysis v0.19.10 // indirect + github.com/go-openapi/errors v0.19.7 // indirect + github.com/go-openapi/jsonpointer v0.19.3 // indirect + github.com/go-openapi/jsonreference v0.19.3 // indirect + github.com/go-openapi/loads v0.19.5 // indirect + github.com/go-openapi/runtime v0.19.16 // indirect + github.com/go-openapi/spec v0.19.8 // indirect + github.com/go-openapi/strfmt v0.19.5 // indirect + github.com/go-openapi/swag v0.19.9 // indirect + github.com/go-openapi/validate v0.19.12 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/gookit/color v1.5.2 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mailru/easyjson v0.7.1 // indirect + github.com/mitchellh/mapstructure v1.3.2 // indirect + github.com/name212/govalue v1.1.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/werf/logboek v0.5.5 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + go.mongodb.org/mongo-driver v1.5.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index c4c1710..389b8bb 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1,10 +1,304 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 h1:HrMVYtly2IVqg9EBooHsakQ256ueojP7QuG32K71X/U= +github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOpfuAKwL5XLFYopbgIl/v7NZxaJpa/4X6yFKE= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckhouse/deckhouse/pkg/log v0.1.1-0.20251230144142-2bad7c3d1edf h1:4HrDzRZcLpREJ+2cSGNmxHVQlxXRcH2r5TGmTcoTZiU= +github.com/deckhouse/deckhouse/pkg/log v0.1.1-0.20251230144142-2bad7c3d1edf/go.mod h1:pbAxTSDcPmwyl3wwKDcEB3qdxHnRxqTV+J0K+sha8bw= +github.com/deckhouse/lib-dhctl v0.11.0 h1:KLxwZ/VdyXdLtHNSYtCdsb5/oQXNs8BVi3i6VzoVcC4= +github.com/deckhouse/lib-dhctl v0.11.0/go.mod h1:RCthjbhLf0CtgdltTmFHk+lRyjRai8BlAO7SocAYR+E= +github.com/deckhouse/lib-gossh v0.3.0 h1:FUAlF8+fLnBCII9hXSNx+arZ4PH3H/6fzp5LBlnmlps= +github.com/deckhouse/lib-gossh v0.3.0/go.mod h1:6bT8jf2fkBPEhYBU35+vMBr5YscliTiS+Vr8v06C+70= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10 h1:5BHISBAXOc/aJK25irLZnx2D3s6WyYaY9D4gmuz9fdE= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7 h1:Lcq+o0mSwCLKACMxZhreVHigB9ebghJ/lrmeaqASbjo= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5 h1:jZVYWawIQiA1NBnHla28ktg6hrcfTHsCE+3QLVRBIls= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.16 h1:tQMAY5s5BfmmCC31+ufDCsGrr8iO1A8UIdYfDo5ADvs= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-openapi/validate v0.19.12 h1:mPLM/bfbd00PGOCJlU0yJL7IulkZ+q9VjPv7U11RMQQ= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/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/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= +github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/name212/govalue v1.1.0 h1:kSdUVs21cM5bFp7RW5sWPrwQ0RzC/Xhk3f+A+dUL6TM= +github.com/name212/govalue v1.1.0/go.mod h1:3mLA4mFb82esucQHCOIAnUjN7e7AZnRYEfxeaHLKjho= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/werf/logboek v0.5.5 h1:RmtTejHJOyw0fub4pIfKsb7OTzD90ZOUyuBAXqYqJpU= +github.com/werf/logboek v0.5.5/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= +go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= +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= +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-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=