diff --git a/README.md b/README.md index aebd2ac..9b4dc1f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Deckhouse connection to nodes over SSH and kube-api over SSH and directly implementations. Library provide interfaces and own implementations for SSH and kubernetes client. -Also library provide special providers for getting clients (more information about this below). +Also, library provide special providers for getting clients (more information about this below). Please DO NOT CREATE implementations of clients directly without need. Please use providers for it. ## Global settings @@ -126,7 +126,7 @@ This files will delete in this call. Also, it stops current client and all addit additional clients. Also, it is safe if some or all clients were stopped. Current client and all additional will remove from provider. Use this method in end of your logic. -Now we have two implementations of `SSHProvider`: `DefaultSSHProvider`, `SSHProvider` in `testssh` package +Now we have three implementations of `SSHProvider`: `DefaultSSHProvider`, `SSHProvider` in `testssh` package and `ErrorSSHProvider`. #### DefaultSSHProvider @@ -141,7 +141,11 @@ or with [parse flags](./pkg/ssh/config/parse_flags.go) or with parse [here](./pkg/ssh/config/openapi/). If you need to provide configuration in your project (for example, render documentation by specs), you can download these schemas in CI or makefile or directly. You can see can you download specs over GitHub API in [makefile](./Makefile) `validation/license/download` -target. +target. +You should not get these schemas for validation. Library embed these schemas and load them if it needs. +But you can get strings of schemas in code with `ConfigurationOpenAPISpec` and +`HostOpenAPISpec` functions. + ###### ParseConnectionConfig @@ -153,7 +157,7 @@ password if password set) and that `legacyMode` and `modernMode` set both. ###### ParseFlags -`FlagsParser` provide `ConnectionConfig` from cli arguments. It is use `https://github.com/spf13/pflag` package for parse it. +`FlagsParser` provide `ConnectionConfig` from cli arguments. It is use [pflag lib](https://github.com/spf13/pflag) for parse it. All flags can rewrite with env variables [described in](./pkg/ssh/config/parse_flags.go). You can provide prefix for envs variables with `WithEnvsPrefix` method. Parse flags doing in next order: ```go @@ -171,32 +175,35 @@ func do() error { if err != nil { return err } - // or you can provide your ouwn arguments slice - err = flags.Parse(os.Args[1:]) - if err != nil { - return err - } // you can use ValidateOption for configure parse - config, err := parser.ExtractConfigAfterParse(flags) + config, err := flags.ExtractConfig(os.Args[1:]) if err != nil { return err } + // if you need to parse you flag set you should parse it by hand + if err := fset.Parse(); err != nil { + return err + } + return nil } ``` -Flags parsers uses copy of passed flag set for parsing. If you need parse with you another flags set -you can get new flag set with `FlagSet` method and parse flag set by your hand. -After parse, extract `ConnectionConfig` with `ExtractConfigAfterParse` method. +Flags parsers uses internal flag set for parsing. If you need parse with you another flags set +you should parse your flag set by your hand. But, parser add "fake" flag set +to passed flag set. It needs for adding information about own flags for out. +If we were using your flag set, parser can add multiple values in slices, for example. +in help. Your flags can be parsed before or after parse own flags. + By default, hosts is not required for parse, you can rewrite with `ParseWithRequiredSSHHost`. It needs because we can parse ssh configuration and kube configuration both and if we have kubeconfig path we should skip all ssh flags and empty flag set for ssh is valid in this case. -But we can use `OverSSH` method in kube configuration. But Warning, you can use ssh routines and kube +But we can use `OverSSH` method in kube configuration. But warning, you can use ssh routines and kube in one logic, and we can use kubeconfig for kube connection. -`ExtractConfigAfterParse` add some defaults if some flags not passes, like port and bastion port (22 by default), +`ExtractConfig` add some defaults if some flags not passes, like port and bastion port (22 by default), user and bastion user (current user from USER env or getting with sys cals). Also, by default flags parser add `~/.ssh/id_rsa` private key. In some cases it is not required: if user uses password auth (without private key) or if user want to use ssh agent private keys only. @@ -258,6 +265,9 @@ password authentification By default, provider not start client if you need you can pass `SSHClientWithStartAfterCreate` option. +`DefaultSSHProvider` init new agent by default for cli-ssh, but if set `ForceUseSSHAgent` new agent does not start. +Also, we can skip run agent with `SSHClientWithNoInitializeAgent` option. + ##### ErrorSSHProvider This provider returns error for every call. This provider can use with `KubeProvider` if you sure @@ -353,4 +363,35 @@ In creation, `FakeKubeProvider` creates current kube-client and returns this cli It needs for test resources if you use additional clients in one place without saving additional clients in your code. You can use `Client` call for getting kube client after test your methods and asserts resources after test. -`KubernetesClient.InitContext` is save for call with fake client \ No newline at end of file +`KubernetesClient.InitContext` is save for call with fake client + +## Flags parse and another examples + +Because we are using [pflag](https://github.com/spf13/pflag) library you can use it with cobra library. + +Full example for init and simple using library provided [here](./examples). +Please show code comments for getting more information about usage of library. + +## Testing + +We added a lot of tests unit and integration. For running full test suit use command: +```bash +make test +``` + +But full test suit is required long time (now about 30 minutes), because we are running ssh and kind containers +for integration testing and tests have long sleeps for prove logic. + +If you do not need to run integration tests you can use: +```bash +make test/no-integration +``` + +In pull requests on GitHub you can use `test/no-integration` label. + +For full cleanup test resource you can use command: +```bash +make clean/test +``` + +It will remove all containers and kind cluster and also remove all temp files. \ No newline at end of file diff --git a/examples/cobra/README.md b/examples/cobra/README.md new file mode 100644 index 0000000..85d6381 --- /dev/null +++ b/examples/cobra/README.md @@ -0,0 +1,18 @@ +# examples/cobra + +This example shows that you can use library with cobra package. + +## Build + +```bash +go build -o cobra main.go +``` + +## Example commands +```bash +./cobra kube-only --tmp-dir=/tmp/my-cobra --kubeconfig=~/my.kind.kubeconfig --kubeconfig-context=kind-my --print-warnin + +./cobra ssh --ssh-user=ubuntu --ssh-host=0.0.0.0 +./cobra ssh --ssh-user=ubuntu --ssh-host=0.0.0.0 --use-standalone-kube --kubeconfig=~/my.kind.kubeconfig +./cobra ssh --ssh-user=ubuntu --ssh-host=0.0.0.0 --ssh-agent-private-keys=~/.ssh/id_rsa --ssh-agent-private-keys=~/.ssh/another +``` \ No newline at end of file diff --git a/examples/cobra/cmd/kube_only.go b/examples/cobra/cmd/kube_only.go new file mode 100644 index 0000000..0cd2408 --- /dev/null +++ b/examples/cobra/cmd/kube_only.go @@ -0,0 +1,167 @@ +// 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 cmd + +import ( + "context" + "fmt" + "time" + + "github.com/deckhouse/lib-connection/pkg/kube" + "github.com/deckhouse/lib-connection/pkg/provider" + "github.com/deckhouse/lib-connection/pkg/settings" + "github.com/deckhouse/lib-dhctl/pkg/retry" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type SettingsProvider func() settings.Settings + +func AppendKubeCommand(settProvider SettingsProvider, parent *cobra.Command) (*cobra.Command, error) { + printWarning := false + + kubeCmd := &cobra.Command{ + Use: "kube-only", + Short: "Run kube example without ssh.", + Long: "Run kube example without ssh.", + } + + // you should add cmd to parent + if parent != nil { + parent.AddCommand(kubeCmd) + } + + // example of usage another flags in command is allowed + // you should use PersistentFlags for getting flags from parent + flagSet := kubeCmd.PersistentFlags() + flagSet.BoolVar(&printWarning, "print-warning", false, "Print warning messages.") + + // initialize our flags + // InitFlags add fake flagSet for: + // prevent of multiple initialization flags + // for available flags in help + // unknown flags not allowed by default + parser := kube.NewFlagsParser(settProvider()) + flags, err := parser.InitFlags(flagSet) + if err != nil { + return nil, err + } + + // flags should pass to handler + // because in handler we have all parsed keys + // and we should extract configs in handler + + kubeCmd.RunE = func(cmd *cobra.Command, args []string) error { + err := runKube(&runKubeParams{ + flags: flags, + printWarn: &printWarning, + settProvider: settProvider, + cmd: cmd, + commandArgs: args, + }) + + if err != nil { + // by default, cobra out usage string + // if command was failed + cmd.SilenceUsage = true + } + + return err + } + + return kubeCmd, nil +} + +type runKubeParams struct { + flags *kube.Flags + printWarn *bool + settProvider SettingsProvider + cmd *cobra.Command + commandArgs []string +} + +func runKube(params *runKubeParams) error { + ctx := params.cmd.Context() + + sett := params.settProvider() + + conf, err := params.flags.ExtractConfig(params.commandArgs...) + if err != nil { + return fmt.Errorf("failed to extract kube provider config: %v", err) + } + + // default initialization way + providerErr := fmt.Errorf("should not use over ssh") + runner, err := provider.GetRunnerInterface(conf, sett, provider.NewErrorSSHProvider(providerErr)) + kubeProvider := provider.NewDefaultKubeProvider(sett, conf, runner) + + // please clean up providers in the end of handler + defer func() { + logger := sett.Logger() + + if err := kubeProvider.Cleanup(ctx); err != nil { + logger.ErrorF("Failed to cleanup kube provider: %v", err) + return + } + + logger.InfoF("kube provider cleaned up successfully") + }() + + // example that additional flags also parsed + if *params.printWarn { + sett.Logger().WarnF("WARNING: printing warnings flag set") + } + + if err != nil { + return fmt.Errorf("failed to setup kube client", err) + } + + if err := getNodes(ctx, sett, kubeProvider); err != nil { + return fmt.Errorf("failed to get nodes: %w", err) + } + + return nil +} + +func getNodes(ctx context.Context, sett settings.Settings, kubeProvider *provider.DefaultKubeProvider) error { + loopParams := retry.NewEmptyParams( + retry.WithName("Getting nodes"), + retry.WithAttempts(5), + retry.WithWait(2*time.Second), + retry.WithLogger(sett.Logger()), + ) + + return retry.NewLoopWithParams(loopParams).RunContext(ctx, func() error { + // please call Client for kube provider in every iteration + // kube provider tracks ssh switches and provide new client if switch happened + client, err := kubeProvider.Client(ctx) + if err != nil { + return fmt.Errorf("cannot extract kube client: %w", err) + } + + nodes, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("cannot list kube nodes: %w", err) + } + + sett.Logger().InfoF("Got kube nodes: %d\n", len(nodes.Items)) + + for _, node := range nodes.Items { + sett.Logger().InfoF("\t%s\n", node.Name) + } + + return nil + }) +} diff --git a/examples/cobra/cmd/with_ssh.go b/examples/cobra/cmd/with_ssh.go new file mode 100644 index 0000000..75a7c8f --- /dev/null +++ b/examples/cobra/cmd/with_ssh.go @@ -0,0 +1,189 @@ +// 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 cmd + +import ( + "fmt" + "os" + + connection "github.com/deckhouse/lib-connection/pkg" + "github.com/deckhouse/lib-connection/pkg/kube" + "github.com/deckhouse/lib-connection/pkg/provider" + sshconfig "github.com/deckhouse/lib-connection/pkg/ssh/config" + "github.com/spf13/cobra" +) + +func AppendSSHCommand(settProvider SettingsProvider, rootCmd *cobra.Command) (*cobra.Command, error) { + sshCmd := &cobra.Command{ + Use: "ssh", + Short: "Run example ssh", + Long: "Run example ssh", + } + + // you should add cmd to parent + if rootCmd != nil { + rootCmd.AddCommand(sshCmd) + } + + // example of usage another flags in command is allowed + // you should use PersistentFlags for getting flags from parent + flagSet := sshCmd.PersistentFlags() + useStandaloneKube := false + flagSet.BoolVar(&useStandaloneKube, "use-standalone-kube", false, "Use provided kube settings not over ssh") + + // default initialization way for flags + + kubeParser := kube.NewFlagsParser(settProvider()) + kubeFlags, err := kubeParser.InitFlags(flagSet) + if err != nil { + return nil, err + } + + sshParser := sshconfig.NewFlagsParser(settProvider()) + sshFlags, err := sshParser.InitFlags(flagSet) + if err != nil { + return nil, err + } + + // flags should pass to handler + // because in handler we have all parsed keys + // and we should extract configs in handler + + sshCmd.RunE = func(cmd *cobra.Command, args []string) error { + err := runSSH(&runSSHParams{ + sshFlags: sshFlags, + kubeFlags: kubeFlags, + useStandaloneKube: &useStandaloneKube, + settProvider: settProvider, + cmd: cmd, + commandArgs: args, + }) + + if err != nil { + cmd.SilenceUsage = true + } + + return err + } + + return sshCmd, nil +} + +type runSSHParams struct { + kubeFlags *kube.Flags + sshFlags *sshconfig.Flags + useStandaloneKube *bool + settProvider SettingsProvider + cmd *cobra.Command + commandArgs []string +} + +func runSSH(params *runSSHParams) error { + ctx := params.cmd.Context() + + sett := params.settProvider() + + kubeConfig, err := params.kubeFlags.ExtractConfig(params.commandArgs...) + if err != nil { + return fmt.Errorf("cannot parse kube config: %w", err) + } + + var sshOpts []sshconfig.ValidateOption + if kubeConfig.OverSSH() { + sshOpts = append(sshOpts, sshconfig.ParseWithRequiredSSHHost(true)) + } + + sshConfig, err := params.sshFlags.ExtractConfig(params.commandArgs, sshOpts...) + if err != nil { + return fmt.Errorf("cannot parse ssh config: %w", err) + } + + sshProvider := provider.NewDefaultSSHProvider(sett, sshConfig, provider.SSHClientWithStartAfterCreate(true)) + + // please clean up providers in the end of handler + defer func() { + logger := sett.Logger() + + if err := sshProvider.Cleanup(ctx); err != nil { + logger.ErrorF("Failed to cleanup ssh provider: %v", err) + return + } + + logger.InfoF("SSH provider cleaned up successfully") + }() + + // example logic if you can use over ssh client and not + // in one handler + // also you can track if you should use over ssh connection + // with kubeConfig.OverSSH() method + // but keep in mind, that your handler can use connection to kube API + // with kubeconfig, but also your handler can do some actions over ssh + var sshProviderForKube connection.SSHProvider = sshProvider + if *params.useStandaloneKube { + providerErr := fmt.Errorf("should not use over ssh") + sshProviderForKube = provider.NewErrorSSHProvider(providerErr) + } + + runner, err := provider.GetRunnerInterface(kubeConfig, sett, sshProviderForKube) + kubeProvider := provider.NewDefaultKubeProvider(sett, kubeConfig, runner) + + // please clean up providers in the end of handler + defer func() { + logger := sett.Logger() + + if err := kubeProvider.Cleanup(ctx); err != nil { + logger.ErrorF("Failed to cleanup kube provider: %v", err) + return + } + + logger.InfoF("kube provider cleaned up successfully") + }() + + if err != nil { + return fmt.Errorf("failed to setup kube client", err) + } + + if err := getNodes(ctx, sett, kubeProvider); err != nil { + return fmt.Errorf("failed to get nodes: %w", err) + } + + notUseSSH := os.Getenv("NOT_USE_SSH") + if notUseSSH != "" { + sett.Logger().InfoF("Not use ssh passed") + return nil + } + + sshClient, err := sshProvider.Client(ctx) + if err != nil { + return fmt.Errorf("failed to setup ssh client: %w", err) + } + + const echoStr = "SUCCESS" + + cmd := sshClient.Command("echo", "-n", echoStr) + cmd.Sudo(ctx) + strOut, _, err := cmd.Output(ctx) + if err != nil { + return fmt.Errorf("failed to run echo command: %w", err) + } + + if string(strOut) != echoStr { + return fmt.Errorf("failed to run echo command, got output: %s", string(strOut)) + } + + sett.Logger().InfoF("SSH command succeeded") + + return nil +} diff --git a/examples/cobra/go.mod b/examples/cobra/go.mod new file mode 100644 index 0000000..7b10ebb --- /dev/null +++ b/examples/cobra/go.mod @@ -0,0 +1,102 @@ +module github.com/deckhouse/lib-connection/examples/cobra + +go 1.25.5 + +require ( + github.com/deckhouse/lib-connection v0.0.0-00010101000000-000000000000 + github.com/deckhouse/lib-dhctl v0.13.0 + github.com/spf13/cobra v1.10.2 + k8s.io/apimachinery v0.32.10 +) + +require ( + al.essio.dev/pkg/shellescape v1.6.0 // indirect + github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bramvdbogaerde/go-scp v1.6.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckhouse/deckhouse/pkg/log v0.1.1-0.20251230144142-2bad7c3d1edf // indirect + github.com/deckhouse/lib-gossh v0.3.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/flant/kube-client v1.5.1 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.4.2 // 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.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // 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.23.0 // indirect + github.com/go-openapi/validate v0.19.12 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gookit/color v1.5.2 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/mapstructure v1.3.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/name212/govalue v1.1.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/werf/logboek v0.5.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // 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 + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.19.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 + golang.org/x/time v0.7.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.10 // indirect + k8s.io/apiextensions-apiserver v0.32.10 // indirect + k8s.io/cli-runtime v0.32.10 // indirect + k8s.io/client-go v0.32.10 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/kustomize/api v0.18.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/deckhouse/lib-connection => ../../ diff --git a/examples/cobra/go.sum b/examples/cobra/go.sum new file mode 100644 index 0000000..8e96d2e --- /dev/null +++ b/examples/cobra/go.sum @@ -0,0 +1,457 @@ +al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= +al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +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/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bramvdbogaerde/go-scp v1.6.0 h1:lDh0lUuz1dbIhJqlKLwWT7tzIRONCp1Mtx3pgQVaLQo= +github.com/bramvdbogaerde/go-scp v1.6.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.13.0 h1:Xh7G30seic4Aa8MdXOH2ecGvO0zsn6PBGBHbJ4YNnZM= +github.com/deckhouse/lib-dhctl v0.13.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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/flant/kube-client v1.5.1 h1:9UTPMxZqAPHUQzWS/4yE5hEPNIYhS+gGmegfi3r2lvQ= +github.com/flant/kube-client v1.5.1/go.mod h1:hpJZ0FnDKHW3r5q5SYQgBrTw9k94q4+dcnJ4uOGYBHc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +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-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +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/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +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/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +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/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +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/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +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/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +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= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +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.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20190620200207-3b0461eec859/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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +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/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-20200930185726-fdedc70b468f/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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/api v0.32.10 h1:ocp4turNfa1V40TuBW/LuA17TeXG9g/GI2ebg0KxBNk= +k8s.io/api v0.32.10/go.mod h1:AsMsc4b6TuampYqgMEGSv0HBFpRS4BlKTXAVCAa7oF4= +k8s.io/apiextensions-apiserver v0.32.10 h1:mAZT8fX/jM9pl7qWkFhhsjQZ8ZkmAhEivfUNw8uKXmo= +k8s.io/apiextensions-apiserver v0.32.10/go.mod h1:wEvqU9kFUQOYminqrroY6+fvSs6iMb7QiiFmcN3b6KY= +k8s.io/apimachinery v0.32.10 h1:SAg2kUPLYRcBJQj66oniP1BnXSqw+l1GvJFsJlBmVvQ= +k8s.io/apimachinery v0.32.10/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/cli-runtime v0.32.10 h1:NdVJeZ27+frB/Gf7siv38nagk2uw1avvWYiq5flv/Yk= +k8s.io/cli-runtime v0.32.10/go.mod h1:4zBnMXj6rsJH8b1DE4TEYBcw+AZ7MtZ4YPaq+EkzVOo= +k8s.io/client-go v0.32.10 h1:MFmIjsKtcnn7mStjrJG1ZW2WzLsKKn6ZtL9hHM/W0xU= +k8s.io/client-go v0.32.10/go.mod h1:qJy/Ws3zSwnu/nD75D+/of1uxbwWHxrYT5P3FuobVLI= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= +sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= +sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= +sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/examples/cobra/hack/kind.yaml b/examples/cobra/hack/kind.yaml new file mode 100644 index 0000000..4150468 --- /dev/null +++ b/examples/cobra/hack/kind.yaml @@ -0,0 +1,4 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane \ No newline at end of file diff --git a/examples/cobra/main.go b/examples/cobra/main.go new file mode 100644 index 0000000..19e8732 --- /dev/null +++ b/examples/cobra/main.go @@ -0,0 +1,194 @@ +// 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 main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/deckhouse/lib-connection/examples/cobra/cmd" + "github.com/deckhouse/lib-connection/pkg/settings" + "github.com/deckhouse/lib-dhctl/pkg/log" + "github.com/spf13/cobra" +) + +type CommandProvider func(cmd.SettingsProvider, *cobra.Command) (*cobra.Command, error) + +type command struct { + shouldContainsInHelp []string + provider CommandProvider +} + +func main() { + const envsPrefix = "COBRA" + + // it needs for running all hooks + // for all parents + cobra.EnableTraverseRunHooks = true + + rootCmd := &cobra.Command{ + Use: "cobra-example", + Short: "Run cobra example.", + Long: "Run cobra example.", + } + + global := &globalArgs{ + loggerType: string(log.Pretty), + tmpDir: filepath.Join(os.TempDir(), "cobra-example"), + } + + // example usage global flags with sub commands using with our flags + rootCmd.PersistentFlags().StringVar(&global.loggerType, "log-type", global.loggerType, "log type") + rootCmd.PersistentFlags().StringVar(&global.tmpDir, "tmp-dir", global.tmpDir, "Temp directory") + + isDebug := os.Getenv(fmt.Sprintf("%s_DEBUG", envsPrefix)) != "" + + logger := log.NewPrettyLogger(log.LoggerOptions{ + IsDebug: isDebug, + }) + + sett := settings.NewBaseProviders(settings.ProviderParams{ + LoggerProvider: log.SimpleLoggerProvider(logger), + IsDebug: isDebug, + EnvsPrefix: envsPrefix, + }) + + // you can use PersistentPreRunE method for initialize globals + // and global library setting. If you use this method, you should + // pass provider settings function to your sub command because + // it rewrite sett variable + rootCmd.PersistentPreRunE = func(*cobra.Command, []string) error { + newSett, err := initSettings(sett, global) + if err != nil { + return err + } + + sett = newSett + + return nil + } + + settingsProvider := func() settings.Settings { + return sett + } + + // shouldContainsInHelp is not necessary + // using for testing help in this example + subCommands := map[string]command{ + "kube": { + provider: cmd.AppendKubeCommand, + shouldContainsInHelp: []string{ + // global flags + "--log-type", + // kube flags + " --kube-client-from-cluster", + // envs prefix + envsPrefix + "_", + }, + }, + "ssh": { + provider: cmd.AppendSSHCommand, + shouldContainsInHelp: []string{ + // global flags + "--tmp-dir", + // kube flags + "--kubeconfig-context", + // ssh flags + "--ssh-legacy-mode", + // envs prefix + envsPrefix + "_", + }, + }, + } + + for name, c := range subCommands { + cc, err := c.provider(settingsProvider, rootCmd) + if err != nil { + sett.Logger().ErrorF("Failed to append command %s: %s", name, err) + os.Exit(1) + return + } + + // unnecessary check + help := cc.UsageString() + for _, h := range c.shouldContainsInHelp { + if !strings.Contains(help, h) { + sett.Logger().ErrorF("Failed to append command %s, help should contains %s", name, h) + os.Exit(1) + } + } + } + + // by default cobra out error + // if you use your logging we can skip it + rootCmd.SilenceErrors = true + rootCmd.TraverseChildren = true + + if err := rootCmd.Execute(); err != nil { + sett.Logger().ErrorF("Failed to execute command: %s", err) + os.Exit(1) + } +} + +type globalArgs struct { + loggerType string + tmpDir string +} + +func initSettings(sett *settings.BaseProviders, args *globalArgs) (*settings.BaseProviders, error) { + tmpDir := args.tmpDir + + sett.Logger().InfoF("Got tmp dir: %s", tmpDir) + sett.Logger().InfoF("Got logger type: %s", args.loggerType) + + if tmpDir == "" || tmpDir == "/" { + return nil, fmt.Errorf("pass incorect tmp dir '%s'", tmpDir) + } + + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + sett.Logger().ErrorF("failed to create tmp dir %s: %v", tmpDir, err) + return nil, err + } + + loggerFromArgs, err := log.NewLogger(log.Type(args.loggerType), sett.IsDebug()) + if err != nil { + return nil, err + } + + sett = sett.Clone( + settings.CloneWithLoggerProvider(log.SimpleLoggerProvider(loggerFromArgs)), + settings.CloneWithTmpDir(tmpDir), + ) + + // unfortunately PersistentPostRun will not run + // if command fail with error, you should use + // global finalize function + // but you should use defer func in sub commands for cleanup + cobra.OnFinalize(func() { + tmpDir = sett.TmpDir() + logger := sett.Logger() + + if err := os.RemoveAll(tmpDir); err != nil { + logger.ErrorF("Failed to remove tmp dir %s: %v", tmpDir, err) + return + } + + logger.InfoF("Tmp dir: '%s' removed", tmpDir) + }) + + return sett, nil +} diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 89310bb..711b24d 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -293,7 +293,7 @@ func IsLive(ctx context.Context, client connection.KubeClient, loopParams ...ret if err == nil { return nil } - return fmt.Errorf("kubernetes API is not Ready: %w", err) + return fmt.Errorf("kubernetes API is not ready: %w", err) }) } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go new file mode 100644 index 0000000..cb1cc58 --- /dev/null +++ b/pkg/kube/client_test.go @@ -0,0 +1,36 @@ +// 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 kube + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFakeClient(t *testing.T) { + t.Run("InitContext does not panics and no rewrite client", func(t *testing.T) { + client := NewFakeKubernetesClient() + innerClient := client.KubeClient + + doInit := func() { + err := client.InitContext(context.TODO(), &Config{}) + require.NoError(t, err, "init context should not fail") + } + require.NotPanics(t, doInit, "init context should not panic") + require.True(t, client.KubeClient == innerClient, "should not rewrite client") + }) +} diff --git a/pkg/kube/parse_flags.go b/pkg/kube/parse_flags.go index c5b2b4a..c9cf1d7 100644 --- a/pkg/kube/parse_flags.go +++ b/pkg/kube/parse_flags.go @@ -44,6 +44,7 @@ type Flags struct { Config baseFlags *baseflags.BaseFlags + parser *FlagsParser } // Parse @@ -97,12 +98,27 @@ func (f *Flags) RewriteFromEnvs() error { return nil } -func (f *Flags) FlagSet() (*flag.FlagSet, error) { - if err := f.baseFlags.IsInitialized(); err != nil { +// ExtractConfig +// if args not passed uses os.Args +func (f *Flags) ExtractConfig(args ...string) (*Config, error) { + if err := f.baseFlags.IsValid(); err != nil { return nil, err } - return f.baseFlags.FlagSet(), nil + if govalue.Nil(f.parser) { + return nil, fmt.Errorf("flag parser cannot be set") + } + + var cmdArgs []string + if len(args) > 0 { + cmdArgs = args + } + + if err := f.baseFlags.Parse(cmdArgs); err != nil { + return nil, err + } + + return f.parser.ExtractConfigAfterParse(f) } type FlagsParser struct { @@ -128,43 +144,27 @@ func (p *FlagsParser) InitFlags(set *flag.FlagSet) (*Flags, error) { return nil, fmt.Errorf("Flags already parsed") } + internalSet := flag.NewFlagSet("lib-connection-kube-internal", flag.ContinueOnError) + envsExtractor := p.NewEnvsExtractor() flags := &Flags{ - baseFlags: baseflags.NewBaseFlags(set, envsExtractor, baseflags.BaseFlagsSkipUnknownFlags()), + baseFlags: baseflags.NewBaseFlags(internalSet, envsExtractor, baseflags.BaseFlagsSkipUnknownFlags()), + parser: p, } - set = flags.baseFlags.FlagSet() + internalSet = flags.baseFlags.FlagSet() - set.StringVar( - &flags.KubeConfig, - kubeConfigFlag, - "", - envsExtractor.AddEnvToUsage( - "Path to kubernetes config file.", - ConfigContextEnv, - ), - ) + p.fillFlagsToSet(internalSet, flags, envsExtractor) - set.StringVar( - &flags.KubeConfigContext, - kubeConfigContextFlag, - "", - envsExtractor.AddEnvToUsage( - "Context from kubernetes config to connect to Kubernetes API.", - ConfigContextEnv, - ), - ) + // we need fake set for prevent writing flags multiple times + // but we should provide helps to parent set + fakeSet := flag.NewFlagSet("lib-connection-kube", flag.ExitOnError) + fakeFlags := &Flags{} - set.BoolVar( - &flags.KubeConfigInCluster, - clientFromClusterFlag, - false, - envsExtractor.AddEnvToUsage( - "Use in-cluster Kubernetes API access.", - ClientFromClusterEnv, - ), - ) + p.fillFlagsToSet(fakeSet, fakeFlags, envsExtractor) + + set.AddFlagSet(fakeSet) return flags, nil } @@ -214,6 +214,9 @@ func (p *FlagsParser) ParseFlagsAndExtractConfig(arguments []string, set *flag.F return nil, err } + internalSet := flags.baseFlags.FlagSet() + internalSet.AddFlagSet(set) + // nil arguments will rewrite from os.Args if err := flags.Parse(arguments); err != nil { return nil, err @@ -222,6 +225,38 @@ func (p *FlagsParser) ParseFlagsAndExtractConfig(arguments []string, set *flag.F return p.ExtractConfigAfterParse(flags) } +func (p *FlagsParser) fillFlagsToSet(set *flag.FlagSet, flags *Flags, envsExtractor *env.Extractor) { + set.StringVar( + &flags.KubeConfig, + kubeConfigFlag, + "", + envsExtractor.AddEnvToUsage( + "Path to kubernetes config file.", + ConfigContextEnv, + ), + ) + + set.StringVar( + &flags.KubeConfigContext, + kubeConfigContextFlag, + "", + envsExtractor.AddEnvToUsage( + "Context from kubernetes config to connect to Kubernetes API.", + ConfigContextEnv, + ), + ) + + set.BoolVar( + &flags.KubeConfigInCluster, + clientFromClusterFlag, + false, + envsExtractor.AddEnvToUsage( + "Use in-cluster Kubernetes API access.", + ClientFromClusterEnv, + ), + ) +} + func (p *FlagsParser) validateKubeConfigWithContext(flags *Flags, logger log.Logger) error { kubeConfigFile := flags.KubeConfig context := flags.KubeConfigContext @@ -234,7 +269,7 @@ func (p *FlagsParser) validateKubeConfigWithContext(flags *Flags, logger log.Log return nil } - content, err := file.ReadFile(kubeConfigFile, "kube config", logger) + content, fullPath, err := file.ReadFile(kubeConfigFile, "kube config", logger) if err != nil { return err } @@ -251,5 +286,7 @@ func (p *FlagsParser) validateKubeConfigWithContext(flags *Flags, logger log.Log } } + flags.KubeConfig = fullPath + return nil } diff --git a/pkg/kube/parse_flags_test.go b/pkg/kube/parse_flags_test.go index 49704a0..08bada6 100644 --- a/pkg/kube/parse_flags_test.go +++ b/pkg/kube/parse_flags_test.go @@ -386,10 +386,7 @@ func TestParseFlags(t *testing.T) { flags, err := parser.InitFlags(fset) require.NoError(t, err, "init flags") - err = flags.Parse(testCase.arguments) - require.NoError(t, err, "should parse flags") - - config, err := parser.ExtractConfigAfterParse(flags) + config, err := flags.ExtractConfig(testCase.arguments...) if assertError(t, err, testCase.hasErrorContains) { return } @@ -598,7 +595,7 @@ func TestParseKubeFlagsAndExtractConfigNoArgs(t *testing.T) { func TestParseFlagsHelp(t *testing.T) { tests.AssertParseFlagsHelp(t, tests.AssertParseFlagsHelpParams{ ExpectedFlags: 3, - Name: "kube-flags", + Name: "lib-connection-kube-internal", Provider: func(sett settings.Settings, envsPrefix string) tests.TestFlagsParser { parser := NewFlagsParser(sett) parser.WithEnvsPrefix(envsPrefix) diff --git a/pkg/provider/ssh.go b/pkg/provider/ssh.go index c4cbaeb..9c4b7c7 100644 --- a/pkg/provider/ssh.go +++ b/pkg/provider/ssh.go @@ -32,6 +32,7 @@ import ( sshconfig "github.com/deckhouse/lib-connection/pkg/ssh/config" "github.com/deckhouse/lib-connection/pkg/ssh/gossh" "github.com/deckhouse/lib-connection/pkg/ssh/session" + "github.com/deckhouse/lib-connection/pkg/utils/file" ) var ( @@ -336,7 +337,13 @@ func (p *DefaultSSHProvider) constructClient(ctx context.Context, sess *session. WithLoopsParams(p.options.LoopsParams).WithID(p.options.ClientID) } - return clissh.NewClient(p.sett, sess, privateKeys, p.options.InitializeNewAgent).WithID(p.options.ClientID) + initNewAgent := p.options.InitializeNewAgent + if p.defaultConfig.Config.ForceUseSSHAgent { + p.debug("Force no init new agent because ForceUseSSHAgent in default config set") + initNewAgent = false + } + + return clissh.NewClient(p.sett, sess, privateKeys, initNewAgent).WithID(p.options.ClientID) } func (p *DefaultSSHProvider) stopCurrentClientIfNeed() { @@ -451,6 +458,10 @@ func (p *DefaultSSHProvider) newSession(parent *session.Session, privateKeys []s input.AvailableHosts = hosts } + if len(input.AvailableHosts) == 0 { + return nil, nil, fmt.Errorf("hosts is empty in session or default config") + } + resPrivateKeys := make([]session.AgentPrivateKey, 0, len(privateKeys)) privateKeysInSession := make(map[string]struct{}) @@ -553,17 +564,18 @@ func (p *DefaultSSHProvider) prepareConfigPrivateKeys() error { func (p *DefaultSSHProvider) appendPrivateKeyPath(key sshconfig.AgentPrivateKey) error { path := key.Key - exists, err := fileExists(path) + var err error + path, err = file.FullPath(path, "private key") if err != nil { return err } - if !exists { - return fmt.Errorf("private key %s does not exist", path) + if err := file.IsExists(path, "private key"); err != nil { + return err } p.defaultPrivateKeysWithPaths = append(p.defaultPrivateKeysWithPaths, session.AgentPrivateKey{ - Key: key.Key, + Key: path, Passphrase: key.Passphrase, }) diff --git a/pkg/provider/ssh_test.go b/pkg/provider/ssh_test.go index df833b4..218b1eb 100644 --- a/pkg/provider/ssh_test.go +++ b/pkg/provider/ssh_test.go @@ -76,6 +76,21 @@ func TestSSHProviderClient(t *testing.T) { assertPrivateKeysAddedInSession(t, client, config.Config.PrivateKeys) }) + t.Run("no pass hosts", func(t *testing.T) { + test := newTest(t) + config := testCreateSSHConnectionConfigWithPrivateKeyPaths(t, connectionConfigParams{ + test: test, + bastionPort: nil, + port: nil, + }) + + config.Hosts = make([]sshconfig.Host, 0) + + provider := newTestProvider(test.Settings(), config) + _, err := provider.Client(context.TODO()) + require.Error(t, err, "client should not be created") + }) + t.Run("private keys paths force cli-ssh no write", func(t *testing.T) { test := newTest(t) config := testCreateSSHConnectionConfigWithPrivateKeyPaths(t, connectionConfigParams{ @@ -256,10 +271,7 @@ func TestSSHProviderClient(t *testing.T) { provider := newTestProvider(sett, config) - expectedErr := fmt.Sprintf( - "Cannot prepare private keys: private key %s does not exist", - notExistsPrivateKeyPath, - ) + expectedErr := "no such file or directory" assertClientAndMultipleClientCall(t, assertParams{ sett: sett, @@ -293,7 +305,7 @@ func TestSSHProviderClient(t *testing.T) { sett: sett, writeKeys: false, provider: provider, - shouldContainError: fmt.Sprintf("Cannot prepare private keys: path %s not regular file", path), + shouldContainError: "should be a file not dir", config: config, }) }) @@ -307,6 +319,35 @@ func TestSSHProviderClient(t *testing.T) { assertSwitchClient(t, params, defaultClient) } + t.Run("no pass hosts to session", func(t *testing.T) { + test := newTest(t) + config := testCreateSSHConnectionConfigWithPrivateKeyPaths(t, connectionConfigParams{ + mode: sshconfig.Mode{ + ForceModern: true, + }, + test: test, + bastionPort: nil, + port: nil, + }) + + ctx := context.TODO() + + provider := newTestProvider(test.Settings(), config) + _, err := provider.Client(ctx) + require.NoError(t, err, "default client should be provided") + + sess := session.NewSession(session.Input{ + User: "default", + }) + + _, err = provider.SwitchClient(ctx, sess, nil) + require.Error(t, err, "switch client should fail") + + client, err := provider.Client(ctx) + require.NoError(t, err, "default client should be provided") + require.False(t, client.IsStopped(), "default client should not be stopped") + }) + t.Run("go-ssh without additional private keys", func(t *testing.T) { test := newTest(t) config := testCreateSSHConnectionConfigWithPrivateKeyPaths(t, connectionConfigParams{ @@ -1267,6 +1308,38 @@ func TestSSHProviderClient(t *testing.T) { ) }) + t.Run("set no init agent if force own agent", func(t *testing.T) { + test := newTest(t) + config := testCreateSSHConnectionConfigWithPrivateKeyPaths(t, connectionConfigParams{ + mode: sshconfig.Mode{ + ForceLegacy: true, + }, + test: test, + bastionPort: nil, + port: nil, + }) + + config.Config.ForceUseSSHAgent = true + + sett := test.Settings() + + provider := newTestProvider(sett, config) + ctx := context.TODO() + + client, err := provider.Client(ctx) + require.NoError(t, err, "should get client") + + cliClient, ok := client.(*clissh.Client) + require.True(t, ok, "client should be cli client") + + require.False(t, cliClient.InitializeNewAgent, "should not initialize new agent") + tests.AssertLogMessage( + t, + sett, + "Force no init new agent because ForceUseSSHAgent in default config set", + ) + }) + t.Run("pass no init agent to cli-ssh", func(t *testing.T) { test := newTest(t) config := testCreateSSHConnectionConfigWithPrivateKeyPaths(t, connectionConfigParams{ diff --git a/pkg/settings/settings.go b/pkg/settings/settings.go index 041dffb..8dca969 100644 --- a/pkg/settings/settings.go +++ b/pkg/settings/settings.go @@ -140,6 +140,18 @@ func CloneWithAuthSock(path string) CloneOpt { } } +func CloneWithLoggerProvider(provider log.LoggerProvider) CloneOpt { + return func(p *BaseProviders) { + p.params.LoggerProvider = provider + } +} + +func CloneWithTmpDir(dir string) CloneOpt { + return func(p *BaseProviders) { + p.params.TmpDir = dir + } +} + func (b *BaseProviders) Clone(opts ...CloneOpt) *BaseProviders { clone := *b diff --git a/pkg/ssh/config/parse_flags.go b/pkg/ssh/config/parse_flags.go index e2cb678..7aeffe4 100644 --- a/pkg/ssh/config/parse_flags.go +++ b/pkg/ssh/config/parse_flags.go @@ -87,6 +87,7 @@ type Flags struct { useAgentWithNoPrivateKeys bool baseFlags *baseflags.BaseFlags + parser *FlagsParser } // Parse @@ -197,12 +198,27 @@ func (f *Flags) RewriteFromEnvs() error { return nil } -func (f *Flags) FlagSet() (*flag.FlagSet, error) { - if err := f.baseFlags.IsInitialized(); err != nil { +// ExtractConfig +// if args is nil used os.Args +func (f *Flags) ExtractConfig(args []string, opts ...ValidateOption) (*ConnectionConfig, error) { + if err := f.baseFlags.IsValid(); err != nil { return nil, err } - return f.baseFlags.FlagSet(), nil + if govalue.Nil(f.parser) { + return nil, fmt.Errorf("flag parser cannot be set") + } + + var cmdArgs []string + if len(args) > 0 { + cmdArgs = args + } + + if err := f.baseFlags.Parse(cmdArgs); err != nil { + return nil, err + } + + return f.parser.ExtractConfigAfterParse(f, opts...) } func (f *Flags) userExtractor() func() (string, error) { @@ -295,14 +311,181 @@ func (p *FlagsParser) InitFlags(set *flag.FlagSet) (*Flags, error) { return nil, fmt.Errorf("Flags already parsed") } + internalSet := flag.NewFlagSet("lib-connection-ssh-internal", flag.ContinueOnError) + envsExtractor := p.NewEnvsExtractor() flags := &Flags{ - baseFlags: baseflags.NewBaseFlags(set, envsExtractor, baseflags.BaseFlagsSkipUnknownFlags()), + baseFlags: baseflags.NewBaseFlags(internalSet, envsExtractor, baseflags.BaseFlagsSkipUnknownFlags()), + parser: p, + } + + internalSet = flags.baseFlags.FlagSet() + + p.fillFlagsToSet(internalSet, flags, envsExtractor) + + // we need fake set for prevent writing flags multiple times + // but we should provide helps to parent set + fakeSet := flag.NewFlagSet("lib-connection-ssh", flag.ExitOnError) + fakeFlags := &Flags{} + + p.fillFlagsToSet(fakeSet, fakeFlags, envsExtractor) + + set.AddFlagSet(fakeSet) + + return flags, nil +} + +// ExtractConfigAfterParse +// extract ConnectionConfig from flags +// Flags contains copy of set. For parse use Flags.Parse +// if flag.FlagSet in Flags is not parse returns error +func (p *FlagsParser) ExtractConfigAfterParse(flags *Flags, opts ...ValidateOption) (*ConnectionConfig, error) { + if err := flags.baseFlags.IsInitialized(); err != nil { + return nil, err + } + + if err := flags.RewriteFromEnvs(); err != nil { + return nil, err + } + + if err := flags.IsConflictBetweenFlags(); err != nil { + return nil, err + } + + sett := p.Settings() + logger := sett.Logger() + + if flags.ConnectionConfigPath != "" { + configReader, err := file.Reader(flags.ConnectionConfigPath, "connection config") + if err != nil { + return nil, err + } + + defer func() { + if err := configReader.Close(); err != nil { + logger.DebugF("Error closing config file: %v", err) + } + }() + + return ParseConnectionConfig(configReader, sett, opts...) + } + + if err := flags.FillDefaults(); err != nil { + return nil, err + } + + options := &validateOptions{} + for _, o := range opts { + o(options) + } + + // TODO prepare connection configuration and use ParseConnectionConfig + // for one place check + // unfortunately we cannot handle error from ParseConnectionConfig + // we should parse error string but it is hard in current time + + hosts := make([]Host, 0, len(flags.Hosts)) + for _, h := range flags.Hosts { + hosts = append(hosts, Host{ + Host: h, + }) + } + + if flags.forceNoPrivateKeys && flags.useAgentWithNoPrivateKeys { + authSockPath := p.Settings().AuthSock() + if err := file.IsExists(authSockPath, "auth socket from env "+settings.SSHAgentAuthSockEnv); err != nil { + return nil, err + } + } + + err := validateOnlyUniqueHosts(hosts, options).flagsError() + if err != nil { + return nil, err + } + + if flags.ForceLegacy && flags.ForceModern { + return nil, fmt.Errorf("--%s and --%s cannot be use both", legacyModeFlag, modernModeFlag) + } + + privateKeys, err := p.readPrivateKeysFromFlags(flags, logger) + if err != nil { + return nil, fmt.Errorf("Failed to read private keys from flags: %w", err) + } + + passwords, err := p.getPasswordsFromUser(flags) + + if err != nil { + return nil, err + } + + res := &ConnectionConfig{ + Config: &Config{ + Mode: Mode{ + ForceLegacy: flags.ForceLegacy, + ForceModern: flags.ForceModern, + }, + + User: flags.User, + Port: intPtr(flags.Port), + + PrivateKeys: privateKeys, + + ExtraArgs: flags.ExtraArgs, + + BastionHost: flags.BastionHost, + BastionPort: intPtr(flags.BastionPort), + BastionUser: flags.BastionUser, + BastionPassword: passwords.Bastion, + + SudoPassword: passwords.Sudo, + + ForceUseSSHAgent: flags.useAgentWithNoPrivateKeys, + }, + Hosts: hosts, + } + + if !res.Config.HaveAuthMethods() { + return nil, fmt.Errorf( + "No auth methods configured. Please pass --%s and/or --%s or --%s with --%s or --%s with --%s", + privateKeysFlag, + askSudoPasswordFlag, + forceNoPrivateKeysFlag, + askSudoPasswordFlag, + forceNoPrivateKeysFlag, + useAgentWithNoPrivateKeysFlag, + ) + } + + return res, nil +} + +// ParseFlagsAndExtractConfig +// initialize, parse and extract ConnectionConfig from flags +// set flag.FlagSet can be nil. If nil, func initialize new flag.FlagSet +// if arguments is nil extract arguments from os.Args +func (p *FlagsParser) ParseFlagsAndExtractConfig(arguments []string, set *flag.FlagSet, opts ...ValidateOption) (*ConnectionConfig, error) { + if govalue.Nil(set) { + set = flag.NewFlagSet("ssh-connection", flag.ExitOnError) + } + + flags, err := p.InitFlags(set) + if err != nil { + return nil, err + } + + internalSet := flags.baseFlags.FlagSet() + internalSet.AddFlagSet(set) + + // nil arguments will rewrite from os.Args + if err := flags.Parse(arguments); err != nil { + return nil, err } - set = flags.baseFlags.FlagSet() + return p.ExtractConfigAfterParse(flags, opts...) +} +func (p *FlagsParser) fillFlagsToSet(set *flag.FlagSet, flags *Flags, envsExtractor *env.Extractor) { set.StringSliceVar( &flags.PrivateKeysPaths, privateKeysFlag, @@ -457,154 +640,6 @@ func (p *FlagsParser) InitFlags(set *flag.FlagSet) (*Flags, error) { UseAgentWithNoPrivateKeysEnv, ), ) - - return flags, nil -} - -// ExtractConfigAfterParse -// extract ConnectionConfig from flags -// Flags contains copy of set. For parse use Flags.Parse -// if flag.FlagSet in Flags is not parse returns error -func (p *FlagsParser) ExtractConfigAfterParse(flags *Flags, opts ...ValidateOption) (*ConnectionConfig, error) { - if err := flags.baseFlags.IsInitialized(); err != nil { - return nil, err - } - - if err := flags.RewriteFromEnvs(); err != nil { - return nil, err - } - - if err := flags.IsConflictBetweenFlags(); err != nil { - return nil, err - } - - sett := p.Settings() - logger := sett.Logger() - - if flags.ConnectionConfigPath != "" { - configReader, err := file.Reader(flags.ConnectionConfigPath, "connection config") - if err != nil { - return nil, err - } - - defer func() { - if err := configReader.Close(); err != nil { - logger.DebugF("Error closing config file: %v", err) - } - }() - - return ParseConnectionConfig(configReader, sett, opts...) - } - - if err := flags.FillDefaults(); err != nil { - return nil, err - } - - options := &validateOptions{} - for _, o := range opts { - o(options) - } - - // TODO prepare connection configuration and use ParseConnectionConfig - // for one place check - // unfortunately we cannot handle error from ParseConnectionConfig - // we should parse error string but it is hard in current time - - hosts := make([]Host, 0, len(flags.Hosts)) - for _, h := range flags.Hosts { - hosts = append(hosts, Host{ - Host: h, - }) - } - - if flags.forceNoPrivateKeys && flags.useAgentWithNoPrivateKeys { - authSockPath := p.Settings().AuthSock() - if err := file.IsExists(authSockPath, "auth socket from env "+settings.SSHAgentAuthSockEnv); err != nil { - return nil, err - } - } - - err := validateOnlyUniqueHosts(hosts, options).flagsError() - if err != nil { - return nil, err - } - - if flags.ForceLegacy && flags.ForceModern { - return nil, fmt.Errorf("--%s and --%s cannot be use both", legacyModeFlag, modernModeFlag) - } - - privateKeys, err := p.readPrivateKeysFromFlags(flags, logger) - if err != nil { - return nil, fmt.Errorf("Failed to read private keys from flags: %w", err) - } - - passwords, err := p.getPasswordsFromUser(flags) - - if err != nil { - return nil, err - } - - res := &ConnectionConfig{ - Config: &Config{ - Mode: Mode{ - ForceLegacy: flags.ForceLegacy, - ForceModern: flags.ForceModern, - }, - - User: flags.User, - Port: intPtr(flags.Port), - - PrivateKeys: privateKeys, - - ExtraArgs: flags.ExtraArgs, - - BastionHost: flags.BastionHost, - BastionPort: intPtr(flags.BastionPort), - BastionUser: flags.BastionUser, - BastionPassword: passwords.Bastion, - - SudoPassword: passwords.Sudo, - - ForceUseSSHAgent: flags.useAgentWithNoPrivateKeys, - }, - Hosts: hosts, - } - - if !res.Config.HaveAuthMethods() { - return nil, fmt.Errorf( - "No auth methods configured. Please pass --%s and/or --%s or --%s with --%s or --%s with --%s", - privateKeysFlag, - askSudoPasswordFlag, - forceNoPrivateKeysFlag, - askSudoPasswordFlag, - forceNoPrivateKeysFlag, - useAgentWithNoPrivateKeysFlag, - ) - } - - return res, nil -} - -// ParseFlagsAndExtractConfig -// initialize, parse and extract ConnectionConfig from flags -// set flag.FlagSet can be nil. If nil, func initialize new flag.FlagSet -// if arguments is nil extract arguments from os.Args -func (p *FlagsParser) ParseFlagsAndExtractConfig(arguments []string, set *flag.FlagSet, opts ...ValidateOption) (*ConnectionConfig, error) { - if govalue.Nil(set) { - set = flag.NewFlagSet("ssh-connection", flag.ExitOnError) - } - - flags, err := p.InitFlags(set) - if err != nil { - return nil, err - } - - // nil arguments will rewrite from os.Args - if err := flags.Parse(arguments); err != nil { - return nil, err - } - - return p.ExtractConfigAfterParse(flags, opts...) } func (p *FlagsParser) readPrivateKeysFromFlags(flags *Flags, logger log.Logger) ([]AgentPrivateKey, error) { @@ -624,14 +659,19 @@ func (p *FlagsParser) readPrivateKeysFromFlags(flags *Flags, logger log.Logger) pathsParsed[path] = struct{}{} - keysPassword, err := p.extractPrivateKey(path, logger) + fullPath, err := file.FullPath(path, "private key") + if err != nil { + return nil, err + } + + keysPassword, err := p.extractPrivateKey(fullPath, logger) if err != nil { parseErr = multierror.Append(parseErr, fmt.Errorf("cannot parse private key file %s: %w", path, err)) continue } res = append(res, AgentPrivateKey{ - Key: path, + Key: fullPath, Passphrase: keysPassword, IsPath: true, }) diff --git a/pkg/ssh/config/parse_flags_test.go b/pkg/ssh/config/parse_flags_test.go index 84b3fa7..4d8e252 100644 --- a/pkg/ssh/config/parse_flags_test.go +++ b/pkg/ssh/config/parse_flags_test.go @@ -61,20 +61,19 @@ func TestParseFlags(t *testing.T) { } type test struct { - name string - passwords *passwordsFromUser - envsPrefix string - envs map[string]string - arguments []string - opts []ValidateOption - hasErrorContains string - hasParseErrorContains string - expected *ConnectionConfig - privateKeys []*testPrivateKey - before func(*testing.T, *test, log.Logger) - privateKeyExtractor PrivateKeyExtractorFunc - test *tests.Test - defaultAsk bool + name string + passwords *passwordsFromUser + envsPrefix string + envs map[string]string + arguments []string + opts []ValidateOption + hasErrorContains string + expected *ConnectionConfig + privateKeys []*testPrivateKey + before func(*testing.T, *test, log.Logger) + privateKeyExtractor PrivateKeyExtractorFunc + test *tests.Test + defaultAsk bool } beforeAddPrivateKeys := func(_ *testing.T, tst *test, _ log.Logger) { @@ -147,8 +146,7 @@ func TestParseFlags(t *testing.T) { "--ssh-host=192.168.0.1", "--unknown=value", }, - hasParseErrorContains: "", - hasErrorContains: "", + hasErrorContains: "", privateKeyExtractor: defaultPrivateKeyExtractor(currentHomeDir), @@ -749,7 +747,7 @@ sshBastionPassword: "not_secure_password_bastion" arguments: []string{ "--ssh-bastion-port=portstr", }, - hasParseErrorContains: `flag: strconv.ParseInt: parsing "portstr": invalid syntax`, + hasErrorContains: `invalid argument "portstr" for "--ssh-bastion-port" flag: strconv.ParseInt: parsing "portstr": invalid syntax`, }, { @@ -1006,16 +1004,7 @@ sshBastionPassword: "not_secure_password_bastion" flags, err := parser.InitFlags(fset) require.NoError(t, err, "init flags") - err = flags.Parse(testCase.arguments) - if testCase.hasParseErrorContains != "" { - require.Error(t, err, "should parse error") - require.Contains(t, err.Error(), testCase.hasParseErrorContains, "should parse error contains") - return - } else { - require.NoError(t, err, "parse flags") - } - - config, err := parser.ExtractConfigAfterParse(flags, testCase.opts...) + config, err := flags.ExtractConfig(testCase.arguments, testCase.opts...) assertConnectionConfig(t, connectionConfigAssertParams{ hasErrorContains: testCase.hasErrorContains, err: err, @@ -1210,7 +1199,7 @@ func TestParseFlagsAndExtractConfigNoArgs(t *testing.T) { func TestParseFlagsHelp(t *testing.T) { tests.AssertParseFlagsHelp(t, tests.AssertParseFlagsHelpParams{ ExpectedFlags: 15, - Name: "ssh-flags", + Name: "lib-connection-ssh-internal", Provider: func(sett settings.Settings, envsPrefix string) tests.TestFlagsParser { parser := NewFlagsParser(sett) parser.WithEnvsPrefix(envsPrefix) diff --git a/pkg/ssh/utils/terminal/ask_password.go b/pkg/ssh/utils/terminal/ask_password.go index e6f910c..50be97d 100644 --- a/pkg/ssh/utils/terminal/ask_password.go +++ b/pkg/ssh/utils/terminal/ask_password.go @@ -29,7 +29,7 @@ func AskPassword(logger log.Logger, prompt string) ([]byte, error) { return nil, fmt.Errorf("stdin is not a terminal, error reading password") } - logger.InfoF(prompt) + logger.InfoFWithoutLn(prompt) data, err := terminal.ReadPassword(fd) logger.InfoF("") diff --git a/pkg/utils/file/reader.go b/pkg/utils/file/reader.go index 7d9dd8b..8dbfbf7 100644 --- a/pkg/utils/file/reader.go +++ b/pkg/utils/file/reader.go @@ -19,23 +19,23 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/deckhouse/lib-dhctl/pkg/log" + + "github.com/deckhouse/lib-connection/pkg/utils/defaults" + "github.com/deckhouse/lib-connection/pkg/utils/env" ) func Reader(path string, fileType string) (io.ReadCloser, error) { - fullPath, err := isExists(path, fileType, true) - if err != nil { - return nil, err - } - - return os.Open(fullPath) + r, _, err := getReader(path, fileType) + return r, err } -func ReadFile(path string, fileType string, logger ...log.Logger) ([]byte, error) { - reader, err := Reader(path, fileType) +func ReadFile(path string, fileType string, logger ...log.Logger) ([]byte, string, error) { + reader, fullPath, err := getReader(path, fileType) if err != nil { - return nil, err + return nil, "", err } defer func() { @@ -45,7 +45,12 @@ func ReadFile(path string, fileType string, logger ...log.Logger) ([]byte, error } }() - return io.ReadAll(reader) + content, err := io.ReadAll(reader) + if err != nil { + return nil, "", err + } + + return content, fullPath, nil } func IsExists(path string, fileType string) error { @@ -54,13 +59,9 @@ func IsExists(path string, fileType string) error { } func isExists(path string, fileType string, shouldRegular bool) (string, error) { - if path == "" { - return "", fmt.Errorf("pass empty path for %s", fileType) - } - - fullPath, err := filepath.Abs(path) + fullPath, err := FullPath(path, fileType) if err != nil { - return "", fmt.Errorf("cannot get abs path for %s: %w", path, err) + return "", err } stat, err := os.Stat(fullPath) @@ -78,3 +79,49 @@ func isExists(path string, fileType string, shouldRegular bool) (string, error) return fullPath, nil } + +func FullPath(path string, fileType string) (string, error) { + if path == "" { + return "", fmt.Errorf("pass empty path for %s", fileType) + } + + fullPath, err := resolveTilda(path, fileType) + if err != nil { + return fullPath, err + } + + fullPath, err = filepath.Abs(fullPath) + if err != nil { + return fullPath, fmt.Errorf("cannot get abs path for %s for type %s: %w", path, fileType, err) + } + + return fullPath, nil +} + +func getReader(path string, fileType string) (io.ReadCloser, string, error) { + fullPath, err := isExists(path, fileType, true) + if err != nil { + return nil, "", err + } + + r, err := os.Open(fullPath) + if err != nil { + return nil, "", err + } + + return r, fullPath, nil +} + +func resolveTilda(path string, fileType string) (string, error) { + if !strings.HasPrefix(path, "~") { + return path, nil + } + + extractor := env.NewOsExtractor("") + home, err := defaults.HomeDir(extractor) + if err != nil { + return "", fmt.Errorf("path %s for %s contains ~ cannot resolve it: %w", path, fileType, err) + } + + return filepath.Join(home, path[1:]), nil +} diff --git a/pkg/utils/flags/base_flags.go b/pkg/utils/flags/base_flags.go index d988fea..78ad36f 100644 --- a/pkg/utils/flags/base_flags.go +++ b/pkg/utils/flags/base_flags.go @@ -72,7 +72,7 @@ func (f *BaseFlags) Parse(args []string) error { return f.flagSet.Parse(args) } -func (f *BaseFlags) IsInitialized() error { +func (f *BaseFlags) IsValid() error { if govalue.Nil(f) { return notInitializedError("baseFlags") } @@ -85,6 +85,14 @@ func (f *BaseFlags) IsInitialized() error { return notInitializedError("flagSet") } + return nil +} + +func (f *BaseFlags) IsInitialized() error { + if err := f.IsValid(); err != nil { + return err + } + if !f.flagSet.Parsed() { return fmt.Errorf("flagsSet is not parsed. Call flag.Parse or flag.FlagSet.Parse before extract config") }