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). Please DO NOT CREATE implementations of clients directly without need. Please use providers for it.
Library routines needs some global settings for running routines.
It describes as Settings interface here. Implementation can create
with NewBaseProviders constructor. Now we have next settings:
LoggerProvider- func that provide logger. By default, uses silent logger If you need debug logs you need to provide logger with debug logging enable.NodeTmpDir- uses for upload bundles and some additional temp files to remote node default -/opt/deckhouse/tmpNodeBinPath- now, uses only for kube-proxy to add this path to PATH env, because we use your own path to safe kubectl on node. Default -/opt/deckhouse/binIsDebug- enable some routines with debugTmpDir- root tmp dir defaultos.TmpDir() + "/dhctl"AuthSock- ssh-agent auth sock, if not set usesos.Getenv("SSH_AUTH_SOCK")for every callEnvsPrefix- envs prefix for flags parsers. Default - empty stringOnShutdown- function to add some routines on end of your logic. Default empty function. You can usetombpackage in dhctl for this.
Interface of SSH client (SSHClient) described here.
With this interface we can run commands, upload and download files, run scripts and bundles,
up tunnel and reverse tunnels and up kubernetes proxy for access to create kubernetes client running over ssh.
Now, we have 3 implementations of SSHClient
- cli - use
sshandscpbinaries for ssh routines. If you use your own bin path for these binaries you should add bin path toPATHenv before use. - go - use own fork of crypto library. We use own with adding additional logging
- testssh - our mock for testing purposes without connection to ssh.
All implementations contain monitors and auto reconnecting to ssh, tunnels and kube-proxy if connection was failed.
Script implementations contains method ExecuteBundle for running script that run list of scripts
named as bundle as output progress of running (see implementation here).
By default, it runs bashible bundle from deckhouse.
If you need run your own bundle pass bundler options BundlerOption with Script.WithBundlerOpts method.
Also library provides Interface interface for running commands and scrip routines on local machine.
Interface of SSH client (KubeClient) described here. It implements client-go client
interface with some additional methods.
Now, we have two implementations of this interface:
- KubernetesClient - use library this implementation can work with kubeconfig, rest client, local run and over SSH with kube-proxy
- ErrorKubernetesClient - it always returns error for all calls. It needs for prevent using closed kube client (more information about this below).
KubeClient can stop with Stop method. If using over SSH connection it stops kube-proxy and client
if passed full flag. Also Stop method switch inner KubeClient to ErrorKubernetesClient for
prevent using closed client and do not additional attempts to kube-proxy.
Library implement own interfaces to provide clients for creating clients for lightweight usage in your routines.
Described here as SSHProvider. Have next interface:
Client- this provides SSH client for default settings passed in provider. Implementations should cache current client. You should use this method for gettingSSHClient. Please do not stop this client directly.SwitchClient- switch currentSSHClientwith new settings. It needs if you first connect with defaults but in you logic we need to use new connection. For example, you connect to master, create new user and should continue working with new user. It will close currentSSHClientif this got viaClientmethod, but safe ifClientdid not call. Warning! This method returnsSSHClient, but DO NOT SAVE it your structures. Please useClientfor getting current client. Example usage:
package my
func do(){
// ini provider
// provider.Client()
// ...
// creating new user over default client
// provider.SwitchClient()
// provider.Client()
// ...
// provider.Client()
// ...
}SwitchToDefault- it uses if you need to use default configuration client afterSwitchClient. For example, For example, you connect to master, create new user do all routines with new user and continue with default. It will close currentSSHClientif this got viaClientorSwitchClientmethod, but safe ifClientor/andSwitchClientdid not call. Warning! This method returnsSSHClient, but DO NOT SAVE it your structures. Please useClientfor getting current client. Example usage:
package my
func do(){
// ini provider
// provider.Client()
// ...
// creating new user over default client
// provider.SwitchClient()
// provider.Client()
// ...
// provider.SwitchToDefault()
// provider.Client()
// delete created user over default client
// ...
}NewAdditionalClient- creates new additional client with default configuration. It needs if you want to use another connection without affect current client. Provider save all clients created via this method for cleanup. If clients does not need anymore you can stop it withStopmethodNewStandaloneClient- creates new standalone client. It needs if you need to connect to another hosts. Provider save all clients created via this method for cleanup. If clients does not need anymore you can stop it withStopmethod.Cleanup- provider can provide some files for its routines like private keys passed from configuration. This files will delete in this call. Also, it stops current client and all additional clients created withNewAdditionalClientandNewStandaloneClient. It is safe if provider does not have current client or 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
and ErrorSSHProvider.
DefaultSSHProvider provide clients with configuration passed default configuration.
Configuration can provide with this.
You can create this configuration (ConnectionConfig struct) directly
or with parse flags or with parse
configuration document. Document schemas described
here. 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 validation/license/download
target.
ParseConnectionConfig gets reader with documents and returns ConnectionConfig struct.
By default, ParseConnectionConfig not allow configuration without hosts and with unknown kinds.
For redeclare it, please use ParseWithRequiredSSHHost and ParseWithSkipUnknownKinds options.
Also, ParseConnectionConfig add some additional checks, like that private keys parsed (with provided
password if password set) and that legacyMode and modernMode set both.
FlagsParser provide ConnectionConfig from cli arguments. It is use https://github.com/spf13/pflag package for parse it.
All flags can rewrite with env variables described in. You can
provide prefix for envs variables with WithEnvsPrefix method. Parse flags doing in next order:
package my
import "os"
func do() error {
// create and prepare parser
parser := NewFlagsParser()
parser.WithEnvsPrefix("DHCTL")
// init flags or you can pass your flagset, parser skip unknown flags
fset := flag.NewFlagSet("my-set", flag.ExitOnError)
flags, err := parser.InitFlags(fset)
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)
if 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.
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
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),
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.
For force use password auth key user should pass --force-no-private-keys with --ask-become-pass flags.
For force only ssh-agent private keys user should pass --force-no-private-keys with
--use-agent-with-no-private-keys flags and set SSH_AUTH_SOCK (in this case parser check that this
env value is exists file).
Flags parser also doing some additional checks for parsed flags:
- private keys files should parse as valid private key. If private key protected with password, parser
ask password for key from terminal. If you need set your own extract logic, please set extractor with
WithPrivateKeyPasswordExtractormethod --ssh-legacy-modeand--ssh-modern-modeshould not provide both- if pass
--ask-become-passor/and--ask-bastion-passparser ask passwords from terminal. If you need set your getting passwords logic, you can provide your func withWithAskmethod, like here:
package my
func do {
// ...
parser.WithAsk(func(promt string) ([]byte, error) {
switch promt {
case "[bastion] Password: ":
return []byte("not secure bastion password"), nil
case "[sudo] Password: ":
return []byte("not secure sudo password"), nil
default:
return nil, fmt.Errorf("unknown prompt %s", promt)
}
})
}- also, parsers checks that auth method was provided (private keys, sudo pass, use agent private keys).
User can pass document file with connection config via --connection-config flag. If this flag provided
parser returns ConnectionConfig parsed with ParseConnectionConfig. If user pass connection config path
with another flags, parser returns error.
If you create ConnectionConfig and want to use ssh-agent only, please set ForceUseSSHAgent field to true.
AgentPrivateKey can proccess Key field as content or file path. If you provide key as file please
set IsPath field to true.
User can pass private keys with ConnectionConfig as file path or content. If it uses as content,
DefaultSSHProvider creates temp files with private keys, because internal logic process private keys
as file. All files will delete on Cleanup call.
Also, in creating all clients (additional, standalone, switch) provider adds private keys from default
configuration by default. For example, if you switch client, you could not add private keys from current
client for safe switching.
DefaultSSHProvider provide client implementations with next rules:
- if you provide
SSHClientWithForceGoSSHoption it returns go-ssh - if set
ForceModernin configuration returns go-ssh - if set
ForceLegacyin configuration returns cli-ssh - if configuration does not contain private keys returns go-ssh, because cli-ssh not supported password authentification
- by default returns cli-ssh. Warning! this behaviour can change in the future.
By default, provider not start client if you need you can pass SSHClientWithStartAfterCreate option.
This provider returns error for every call. This provider can use with KubeProvider if you sure
that you need to use kube client not over ssh.
You can pass this provider in unit tests. This provider save all switch calls and you can test it.
Provides kubernetes client. Have next methods:
Client- gets current client or init new if current client not set. Client cached. If you client in retry loop, please callClienton every iteration. And please do not save client in your structures, please callClientwith every kube-api routine. And do not stop this client directly.NewAdditionalClient- initialize new client. Need use if you do not want affect current client. If you do not need a client, you can callkube.Stopmethod for stop client and its inferiors. All clients created with this method saved in provider.NewAdditionalClientWithoutInitialize- create new client, but not initialize it. For start client please useclient.InitContext. Need use if you do not want affect current client. All clients created with this method saved in provider.Cleanup- stops all additional clients got from NewAdditionalClient and NewAdditionalClientWithoutInitialize also current client also stop, but not fully because if we use over ssh current client can use in another routines. CallCleanupis safe for call on stopped clients.
Now, we have next implementations:
DefaultKubeProvider- provide default client with its configFakeKubeProvider- provide fake clients for using in tests.
DefaultKubeProvider creates kube provider dependent on passed user configuration. Configuration described here
Kube client creates with in next order:
- if set
config.KubeConfigInClusterprovider will usein-clusterconfiguration. This should use for creating kube client in containers in k8s cluster - if set
config.KubeConfig(path to kubeconfig) uses this kubeconfig for connection - if set
config.RestConfiguses this configuration for connect to kube API. It needs if you want to use BearerToken for connect. - if set
config.LocalKubeClientuse directly connection on same host - by default uses kube proxy over ssh.
You can use kube.FlagsParser for extract configuration from cli flags.
This parser have same rules as ssh flags parser. Client can provide
kubeconfig path with context in kubeconfig or in-cluster mode only. For another options like
local or rest config you can prepare configuration in code.
FlagsParser have next additional checks:
- fail if
in-clustermode pass with kubeconfig path - if kubeconfig provided, parser checks that provide valid kubeconfig
- if pass context, provider checks that kubeconfig contains this context.
Warning! Parser also checks KUBECONFIG env. If this env sets, parser use value from env as
kubeconfig path.
For init provider, you can pass special interface RunnerInterface this interface provide routines
to additional logic used depend on configuration. For getting implementation use GetRunnerInterface
This function checks that configuration is not conflicted (use one connection method)
For kubeconfig, in-cluster and rest config modes, implementations does not contain complex logic.
But for ssh logic is complex.
RunnerInterfaceSSH got SSHProvider for provide client for starting kube-proxy
For call Client provider (in fact RunnerInterfaceSSH) use SSHProvider.Client() method.
For every call, provider checks that ssh-client configuration is same with current.
If it is same, returns current saved kube client. Otherwise, provider initialize new kube client
with got SSHClient. Also, during initialization it checks that ssh host available and switch to
another host if it needs. After initialize new kube client stops current kube-client, but not fully.
This logic needs for simple usage KubeProvider you do not need track ssh switches in your logic.
And that's why you need Client call for every kube API interaction.
NewAdditionalClient and NewAdditionalClientWithoutInitialize always create new ssh-client with
sshProvider.NewAdditionalClient. That's why you can stop this kube-clients fully. All these clients
saved to internals for cleanup.
Before returns new kube-client, provider checks, that kube API is available.
Cleanup - stops all additional clients fully, but current stop not fully (only kube-proxy), because
current kube-client uses current ssh-client but this client can use in the next operations in your code.
Provides fake kube client.
In creation, FakeKubeProvider creates current kube-client and returns this client for all methods.
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