Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 57 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
`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.
18 changes: 18 additions & 0 deletions examples/cobra/README.md
Original file line number Diff line number Diff line change
@@ -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
```
167 changes: 167 additions & 0 deletions examples/cobra/cmd/kube_only.go
Original file line number Diff line number Diff line change
@@ -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
})
}
Loading