forked from coop-cloud/abra
parent
3dc186e231
commit
edb427a7ae
|
@ -17,13 +17,13 @@ import (
|
|||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
"coopcloud.tech/abra/pkg/server"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sfreiberg/simplessh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
@ -166,7 +166,7 @@ func newLocalServer(c *cli.Context, domainName string) error {
|
|||
}
|
||||
|
||||
func newContext(c *cli.Context, domainName, username, port string) error {
|
||||
store := client.NewDefaultDockerContextStore()
|
||||
store := contextPkg.NewDefaultDockerContextStore()
|
||||
contexts, err := store.Store.List()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -196,7 +196,7 @@ func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error)
|
|||
return cl, nil
|
||||
}
|
||||
|
||||
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *simplessh.Client, domainName string) error {
|
||||
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error {
|
||||
result, err := sshCl.Exec("which docker")
|
||||
if err != nil && string(result) != "" {
|
||||
return err
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
@ -18,7 +18,7 @@ var serverListCommand = &cli.Command{
|
|||
ArgsUsage: " ",
|
||||
HideHelp: true,
|
||||
Action: func(c *cli.Context) error {
|
||||
dockerContextStore := client.NewDefaultDockerContextStore()
|
||||
dockerContextStore := context.NewDefaultDockerContextStore()
|
||||
contexts, err := dockerContextStore.Store.List()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
@ -36,7 +36,7 @@ var serverListCommand = &cli.Command{
|
|||
for _, serverName := range serverNames {
|
||||
var row []string
|
||||
for _, ctx := range contexts {
|
||||
endpoint, err := client.GetContextEndpoint(ctx)
|
||||
endpoint, err := context.GetContextEndpoint(ctx)
|
||||
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, we can continue safely
|
||||
continue
|
||||
|
|
5
go.mod
5
go.mod
|
@ -28,10 +28,10 @@ require (
|
|||
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e
|
||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||
github.com/containerd/containerd v1.5.5 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351
|
||||
|
@ -40,9 +40,8 @@ require (
|
|||
github.com/moby/sys/mount v0.2.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/pkg/sftp v1.13.4 // indirect
|
||||
github.com/sfreiberg/simplessh v0.0.0-20180301191542-495cbb862a9c
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0
|
||||
)
|
||||
|
|
11
go.sum
11
go.sum
|
@ -253,8 +253,6 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
|
@ -304,6 +302,7 @@ github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
|
|||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
|
@ -501,8 +500,6 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
|
|||
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/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
|
@ -643,8 +640,6 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
|
|||
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/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
|
||||
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
|
@ -699,8 +694,6 @@ github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60J
|
|||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sfreiberg/simplessh v0.0.0-20180301191542-495cbb862a9c h1:7Q+2oF0uBoLEV+j13E3/xUkPkI7f+sFNPZOPo2jmrWk=
|
||||
github.com/sfreiberg/simplessh v0.0.0-20180301191542-495cbb862a9c/go.mod h1:sB7d6wQapoRM+qx5MgQYB6JVHtel4YHRr0NXXCkXiwQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
|
@ -827,7 +820,6 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -974,7 +966,6 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -21,7 +22,7 @@ func New(contextName string) (*client.Client, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
ctxEndpoint, err := GetContextEndpoint(context)
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -4,14 +4,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
command "github.com/docker/cli/cli/command"
|
||||
dConfig "github.com/docker/cli/cli/config"
|
||||
context "github.com/docker/cli/cli/context"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
contextStore "github.com/docker/cli/cli/context/store"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -35,7 +32,7 @@ func CreateContext(contextName string, user string, port string) error {
|
|||
|
||||
// createContext interacts with Docker Context to create a Docker context config
|
||||
func createContext(name string, host string) error {
|
||||
s := NewDefaultDockerContextStore()
|
||||
s := context.NewDefaultDockerContextStore()
|
||||
contextMetadata := contextStore.Metadata{
|
||||
Endpoints: make(map[string]interface{}),
|
||||
Name: name,
|
||||
|
@ -83,46 +80,14 @@ func DeleteContext(name string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return NewDefaultDockerContextStore().Remove(name)
|
||||
return context.NewDefaultDockerContextStore().Remove(name)
|
||||
}
|
||||
|
||||
func GetContext(contextName string) (contextStore.Metadata, error) {
|
||||
ctx, err := NewDefaultDockerContextStore().GetMetadata(contextName)
|
||||
ctx, err := context.NewDefaultDockerContextStore().GetMetadata(contextName)
|
||||
if err != nil {
|
||||
return contextStore.Metadata{}, err
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
|
||||
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
|
||||
if !ok {
|
||||
err := errors.New("context lacks Docker endpoint")
|
||||
return "", err
|
||||
}
|
||||
return endpointmeta.Host, nil
|
||||
}
|
||||
|
||||
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
|
||||
return contextStore.New(dir, config)
|
||||
}
|
||||
|
||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
|
||||
_, _, stderr := term.StdStreams()
|
||||
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
||||
contextDir := dConfig.ContextStoreDir()
|
||||
storeConfig := command.DefaultContextStoreConfig()
|
||||
store := newContextStore(contextDir, storeConfig)
|
||||
|
||||
opts := &cliflags.CommonOptions{Context: "default"}
|
||||
|
||||
dockerContextStore := &command.ContextStoreWithDefault{
|
||||
Store: store,
|
||||
Resolver: func() (*command.DefaultContext, error) {
|
||||
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
|
||||
},
|
||||
}
|
||||
|
||||
return dockerContextStore
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
dContext "github.com/docker/cli/cli/context"
|
||||
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||
)
|
||||
|
@ -64,7 +65,7 @@ func TestGetContextEndpoint(t *testing.T) {
|
|||
dockerContext("ssh://foobar", "k8"),
|
||||
}
|
||||
for _, context := range testDockerContexts {
|
||||
endpoint, err := client.GetContextEndpoint(context.context)
|
||||
endpoint, err := contextPkg.GetContextEndpoint(context.context)
|
||||
if err != nil {
|
||||
if err.Error() != "context lacks Docker endpoint" {
|
||||
t.Error(err)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
|
@ -145,6 +146,10 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
|
|||
|
||||
logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
|
||||
|
||||
if err := EnsureHostKeysAllServers(servers...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
serverDir := path.Join(ABRA_SERVER_FOLDER, server)
|
||||
files, err := getAllFilesInDirectory(serverDir)
|
||||
|
@ -368,3 +373,15 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
|
|||
|
||||
return compose, nil
|
||||
}
|
||||
|
||||
// EnsureHostKeysAllServers ensures all configured servers have server SSH host keys validated
|
||||
func EnsureHostKeysAllServers(servers ...string) error {
|
||||
for _, serverName := range servers {
|
||||
logrus.Debugf("ensuring server SSH host key available for %s", serverName)
|
||||
if err := ssh.EnsureHostKey(serverName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
dConfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/context"
|
||||
contextStore "github.com/docker/cli/cli/context/store"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
|
||||
_, _, stderr := term.StdStreams()
|
||||
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
||||
contextDir := dConfig.ContextStoreDir()
|
||||
storeConfig := command.DefaultContextStoreConfig()
|
||||
store := newContextStore(contextDir, storeConfig)
|
||||
|
||||
opts := &cliflags.CommonOptions{Context: "default"}
|
||||
|
||||
dockerContextStore := &command.ContextStoreWithDefault{
|
||||
Store: store,
|
||||
Resolver: func() (*command.DefaultContext, error) {
|
||||
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
|
||||
},
|
||||
}
|
||||
|
||||
return dockerContextStore
|
||||
}
|
||||
|
||||
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
|
||||
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
|
||||
if !ok {
|
||||
err := errors.New("context lacks Docker endpoint")
|
||||
return "", err
|
||||
}
|
||||
return endpointmeta.Host, nil
|
||||
}
|
||||
|
||||
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
|
||||
return contextStore.New(dir, config)
|
||||
}
|
435
pkg/ssh/ssh.go
435
pkg/ssh/ssh.go
|
@ -3,18 +3,35 @@ package ssh
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
dockerSSHPkg "github.com/docker/cli/cli/connhelper/ssh"
|
||||
sshPkg "github.com/gliderlabs/ssh"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/sfreiberg/simplessh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
var KnownHostsPath = filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
|
||||
|
||||
type Client struct {
|
||||
SSHClient *ssh.Client
|
||||
}
|
||||
|
||||
// HostConfig is a SSH host config.
|
||||
type HostConfig struct {
|
||||
Host string
|
||||
|
@ -23,61 +40,39 @@ type HostConfig struct {
|
|||
User string
|
||||
}
|
||||
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host.
|
||||
func GetHostConfig(hostname, username, port string) (HostConfig, error) {
|
||||
var hostConfig HostConfig
|
||||
|
||||
var host, idf string
|
||||
|
||||
if host = ssh_config.Get(hostname, "Hostname"); host == "" {
|
||||
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname)
|
||||
host = hostname
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
if username = ssh_config.Get(hostname, "User"); username == "" {
|
||||
systemUser, err := user.Current()
|
||||
// Exec cmd on the remote host and return stderr and stdout
|
||||
func (c *Client) Exec(cmd string) ([]byte, error) {
|
||||
session, err := c.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
|
||||
username = systemUser.Username
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
if port = ssh_config.Get(hostname, "Port"); port == "" {
|
||||
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22")
|
||||
port = "22"
|
||||
}
|
||||
}
|
||||
|
||||
idf = ssh_config.Get(hostname, "IdentityFile")
|
||||
|
||||
hostConfig.Host = host
|
||||
if idf != "" {
|
||||
hostConfig.IdentityFile = idf
|
||||
}
|
||||
hostConfig.Port = port
|
||||
hostConfig.User = username
|
||||
|
||||
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
|
||||
|
||||
return hostConfig, nil
|
||||
// Close the underlying SSH connection
|
||||
func (c *Client) Close() error {
|
||||
return c.SSHClient.Close()
|
||||
}
|
||||
|
||||
// New creates a new SSH client connection.
|
||||
func New(domainName, sshAuth, username, port string) (*simplessh.Client, error) {
|
||||
var client *simplessh.Client
|
||||
func New(domainName, sshAuth, username, port string) (*Client, error) {
|
||||
var client *Client
|
||||
|
||||
hostConfig, err := GetHostConfig(domainName, username, port)
|
||||
ctxConnDetails, err := GetContextConnDetails(domainName)
|
||||
if err != nil {
|
||||
return client, err
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if sshAuth == "identity-file" {
|
||||
var err error
|
||||
client, err = simplessh.ConnectWithAgentTimeout(hostConfig.Host, hostConfig.User, 5*time.Second)
|
||||
client, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
@ -91,7 +86,13 @@ func New(domainName, sshAuth, username, port string) (*simplessh.Client, error)
|
|||
}
|
||||
|
||||
var err error
|
||||
client, err = simplessh.ConnectWithPasswordTimeout(hostConfig.Host, hostConfig.User, password, 5*time.Second)
|
||||
client, err = connectWithPasswordTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
password,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
@ -100,8 +101,7 @@ func New(domainName, sshAuth, username, port string) (*simplessh.Client, error)
|
|||
return client, nil
|
||||
}
|
||||
|
||||
// sudoWriter supports sudo command handling.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
// sudoWriter supports sudo command handling
|
||||
type sudoWriter struct {
|
||||
b bytes.Buffer
|
||||
pw string
|
||||
|
@ -109,8 +109,7 @@ type sudoWriter struct {
|
|||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Write satisfies the write interface for sudoWriter.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
// Write satisfies the write interface for sudoWriter
|
||||
func (w *sudoWriter) Write(p []byte) (int, error) {
|
||||
if string(p) == "sudo_password" {
|
||||
w.stdin.Write([]byte(w.pw + "\n"))
|
||||
|
@ -124,9 +123,8 @@ func (w *sudoWriter) Write(p []byte) (int, error) {
|
|||
return w.b.Write(p)
|
||||
}
|
||||
|
||||
// RunSudoCmd runs SSH commands and streams output.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
func RunSudoCmd(cmd, passwd string, cl *simplessh.Client) error {
|
||||
// RunSudoCmd runs SSH commands and streams output
|
||||
func RunSudoCmd(cmd, passwd string, cl *Client) error {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -170,9 +168,8 @@ func RunSudoCmd(cmd, passwd string, cl *simplessh.Client) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Exec runs a command on a remote and streams output.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
func Exec(cmd string, cl *simplessh.Client) error {
|
||||
// Exec runs a command on a remote and streams output
|
||||
func Exec(cmd string, cl *Client) error {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -224,3 +221,333 @@ func Exec(cmd string, cl *simplessh.Client) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureKnowHostsFiles ensures that ~/.ssh/known_hosts is created
|
||||
func EnsureKnowHostsFiles() error {
|
||||
if _, err := os.Stat(KnownHostsPath); os.IsNotExist(err) {
|
||||
logrus.Debugf("missing %s, creating now", KnownHostsPath)
|
||||
file, err := os.OpenFile(KnownHostsPath, os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHostKey checks if a host key is registered in the ~/.ssh/known_hosts file
|
||||
func GetHostKey(hostname string) (bool, sshPkg.PublicKey, error) {
|
||||
var hostKey sshPkg.PublicKey
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(hostname)
|
||||
if err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
|
||||
if err := EnsureKnowHostsFiles(); err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
|
||||
file, err := os.Open(KnownHostsPath)
|
||||
if err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Split(scanner.Text(), " ")
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
hostnameAndPort := fmt.Sprintf("%s:%s", ctxConnDetails.Host, ctxConnDetails.Port)
|
||||
hashed := knownhosts.Normalize(hostnameAndPort)
|
||||
|
||||
if strings.Contains(fields[0], hashed) {
|
||||
var err error
|
||||
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
|
||||
if err != nil {
|
||||
return false, hostKey, fmt.Errorf("error parsing server SSH host key %q: %v", fields[2], err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hostKey != nil {
|
||||
logrus.Debugf("server SSH host key present in ~/.ssh/known_hosts for %s", hostname)
|
||||
return true, hostKey, nil
|
||||
}
|
||||
|
||||
return false, hostKey, nil
|
||||
}
|
||||
|
||||
// InsertHostKey adds a new host key to the ~/.ssh/known_hosts file
|
||||
func InsertHostKey(hostname string, remote net.Addr, pubKey ssh.PublicKey) error {
|
||||
file, err := os.OpenFile(KnownHostsPath, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hashedHostname := knownhosts.Normalize(hostname)
|
||||
lineHostname := knownhosts.Line([]string{hashedHostname}, pubKey)
|
||||
_, err = file.WriteString(fmt.Sprintf("%s\n", lineHostname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedRemote := knownhosts.Normalize(remote.String())
|
||||
lineRemote := knownhosts.Line([]string{hashedRemote}, pubKey)
|
||||
_, err = file.WriteString(fmt.Sprintf("%s\n", lineRemote))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("SSH host key generated: %s", lineHostname)
|
||||
logrus.Debugf("SSH host key generated: %s", lineRemote)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostKeyAddCallback ensures server ssh host keys are handled
|
||||
func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.PublicKey) error {
|
||||
exists, _, err := GetHostKey(hostnameAndPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
hostname := strings.Split(hostnameAndPort, ":")[0]
|
||||
logrus.Debugf("server SSH host key found for %s, moving on", hostname)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !exists {
|
||||
hostname := strings.Split(hostnameAndPort, ":")[0]
|
||||
parsedPubKey := FingerprintSHA256(pubKey)
|
||||
|
||||
fmt.Printf(fmt.Sprintf(`
|
||||
You are attempting to make an SSH connection to a server but there is no entry
|
||||
in your ~/.ssh/known_hosts file which confirms that this is indeed the server
|
||||
you want to connect to. Please take a moment to validate the following SSH host
|
||||
key, it is important.
|
||||
|
||||
Host: %s
|
||||
Fingerprint: %s
|
||||
|
||||
If this is confusing to you, you can read the article below and learn how to
|
||||
validate this fingerprint safely. Thanks to the comrades at cyberia.club for
|
||||
writing this extensive guide <3
|
||||
|
||||
https://sequentialread.com/understanding-the-secure-shell-protocol-ssh/
|
||||
|
||||
`, hostname, parsedPubKey))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "are you sure you trust this host key?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
logrus.Debugf("attempting to insert server SSH host key for %s, %s", hostnameAndPort, remote)
|
||||
|
||||
if err := InsertHostKey(hostnameAndPort, remote, pubKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("successfully added server SSH host key for %s", hostname)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// connect makes the SSH connection
|
||||
func connect(username, host, port string, authMethod ssh.AuthMethod, timeout time.Duration) (*Client, error) {
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: HostKeyAddCallback, // the main reason why we fork
|
||||
}
|
||||
|
||||
hostnameAndPort := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
logrus.Debugf("tcp dialing %s", hostnameAndPort)
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
conn, err = net.DialTimeout("tcp", hostnameAndPort, timeout)
|
||||
if err != nil {
|
||||
logrus.Debugf("tcp dialing %s failed, trying via ~/.ssh/config", hostnameAndPort)
|
||||
hostConfig, err := GetHostConfig(host, username, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err = net.DialTimeout("tcp", fmt.Sprintf("%s:%s", hostConfig.Host, hostConfig.Port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, hostnameAndPort, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
c := &Client{SSHClient: client}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func connectWithAgentTimeout(host, username, port string, timeout time.Duration) (*Client, error) {
|
||||
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authMethod := ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
|
||||
|
||||
return connect(username, host, port, authMethod, timeout)
|
||||
}
|
||||
|
||||
func connectWithPasswordTimeout(host, username, port, pass string, timeout time.Duration) (*Client, error) {
|
||||
authMethod := ssh.Password(pass)
|
||||
|
||||
return connect(username, host, port, authMethod, timeout)
|
||||
}
|
||||
|
||||
// EnsureHostKey ensures that a host key trusted and added to the ~/.ssh/known_hosts file
|
||||
func EnsureHostKey(hostname string) error {
|
||||
exists, _, err := GetHostKey(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FingerprintSHA256 generates the SHA256 fingerprint for a server SSH host key
|
||||
func FingerprintSHA256(key ssh.PublicKey) string {
|
||||
hash := sha256.Sum256(key.Marshal())
|
||||
b64hash := base64.StdEncoding.EncodeToString(hash[:])
|
||||
trimmed := strings.TrimRight(b64hash, "=")
|
||||
return fmt.Sprintf("SHA256:%s", trimmed)
|
||||
}
|
||||
|
||||
// GetContextConnDetails retrieves SSH connection details from a docker context endpoint
|
||||
func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
|
||||
dockerContextStore := context.NewDefaultDockerContextStore()
|
||||
contexts, err := dockerContextStore.Store.List()
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
|
||||
if strings.Contains(serverName, ":") {
|
||||
serverName = strings.Split(serverName, ":")[0]
|
||||
}
|
||||
|
||||
for _, ctx := range contexts {
|
||||
endpoint, err := context.GetContextEndpoint(ctx)
|
||||
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, we can continue safely
|
||||
continue
|
||||
}
|
||||
if ctx.Name == serverName {
|
||||
ctxConnDetails, err := dockerSSHPkg.ParseURL(endpoint)
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
logrus.Debugf("found context connection details %v for %s", ctxConnDetails, serverName)
|
||||
return ctxConnDetails, nil
|
||||
}
|
||||
}
|
||||
|
||||
hostConfig, err := GetHostConfig(serverName, "", "")
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
|
||||
logrus.Debugf("couldn't find a docker context matching %s", serverName)
|
||||
logrus.Debugf("searching ~/.ssh/config for a Host entry for %s", serverName)
|
||||
|
||||
connDetails := &dockerSSHPkg.Spec{
|
||||
Host: hostConfig.Host,
|
||||
User: hostConfig.User,
|
||||
Port: hostConfig.Port,
|
||||
}
|
||||
|
||||
logrus.Debugf("using %v from ~/.ssh/config for connection details", connDetails)
|
||||
|
||||
return connDetails, nil
|
||||
}
|
||||
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host.
|
||||
func GetHostConfig(hostname, username, port string) (HostConfig, error) {
|
||||
var hostConfig HostConfig
|
||||
|
||||
var host, idf string
|
||||
|
||||
if host = ssh_config.Get(hostname, "Hostname"); host == "" {
|
||||
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname)
|
||||
host = hostname
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
if username = ssh_config.Get(hostname, "User"); username == "" {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
|
||||
username = systemUser.Username
|
||||
}
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
if port = ssh_config.Get(hostname, "Port"); port == "" {
|
||||
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22")
|
||||
port = "22"
|
||||
}
|
||||
}
|
||||
|
||||
idf = ssh_config.Get(hostname, "IdentityFile")
|
||||
|
||||
hostConfig.Host = host
|
||||
if idf != "" {
|
||||
hostConfig.IdentityFile = idf
|
||||
}
|
||||
hostConfig.Port = port
|
||||
hostConfig.User = username
|
||||
|
||||
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
|
||||
|
||||
return hostConfig, nil
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net"
|
||||
"net/url"
|
||||
|
||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
|
@ -19,23 +20,26 @@ import (
|
|||
//
|
||||
// ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host.
|
||||
func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) {
|
||||
return getConnectionHelper(daemonURL, []string{"-o ConnectTimeout=5", "-o StrictHostKeyChecking=no"})
|
||||
return getConnectionHelper(daemonURL, []string{"-o ConnectTimeout=5"})
|
||||
}
|
||||
|
||||
func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) {
|
||||
u, err := url.Parse(daemonURL)
|
||||
url, err := url.Parse(daemonURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch scheme := u.Scheme; scheme {
|
||||
switch scheme := url.Scheme; scheme {
|
||||
case "ssh":
|
||||
sp, err := ssh.ParseURL(daemonURL)
|
||||
ctxConnDetails, err := ssh.ParseURL(daemonURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &connhelper.ConnectionHelper{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return New(ctx, "ssh", append(sshFlags, sp.Args("docker", "system", "dial-stdio")...)...)
|
||||
return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...)
|
||||
},
|
||||
Host: "http://docker.example.com",
|
||||
}, nil
|
||||
|
|
Loading…
Reference in New Issue