forked from toolshed/abra
refactor!: consolidate SSH handling
Closes coop-cloud/organising#389. Closes coop-cloud/organising#341. Closes coop-cloud/organising#326. Closes coop-cloud/organising#380. Closes coop-cloud/organising#360.
This commit is contained in:
@ -2,22 +2,29 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// New initiates a new Docker client.
|
||||
func New(contextName string) (*client.Client, error) {
|
||||
// New initiates a new Docker client. New client connections are validated so
|
||||
// that we ensure connections via SSH to the daemon can succeed. It takes into
|
||||
// account that you may only want the local client and not communicate via SSH.
|
||||
// For this use-case, please pass "default" as the contextName.
|
||||
func New(serverName string) (*client.Client, error) {
|
||||
var clientOpts []client.Opt
|
||||
|
||||
if contextName != "default" {
|
||||
context, err := GetContext(contextName)
|
||||
if serverName != "default" {
|
||||
context, err := GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -33,7 +40,6 @@ func New(contextName string) (*client.Client, error) {
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
// No tls, no proxy
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
@ -59,7 +65,20 @@ func New(contextName string) (*client.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Debugf("created client for %s", contextName)
|
||||
logrus.Debugf("created client for %s", serverName)
|
||||
|
||||
info, err := cl.Info(context.Background())
|
||||
if err != nil {
|
||||
return cl, sshPkg.Fatal(serverName, err)
|
||||
}
|
||||
|
||||
if info.Swarm.LocalNodeState == "inactive" {
|
||||
if serverName != "default" {
|
||||
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName)
|
||||
} else {
|
||||
return cl, errors.New("swarm mode not enabled on local server?")
|
||||
}
|
||||
}
|
||||
|
||||
return cl, nil
|
||||
}
|
||||
|
@ -4,20 +4,14 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func StoreSecret(secretName, secretValue, server string) error {
|
||||
cl, err := New(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
func StoreSecret(cl *client.Client, secretName, secretValue, server string) error {
|
||||
ann := swarm.Annotations{Name: secretName}
|
||||
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
|
||||
|
||||
// We don't bother with the secret IDs for now
|
||||
if _, err := cl.SecretCreate(ctx, spec); err != nil {
|
||||
if _, err := cl.SecretCreate(context.Background(), spec); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,10 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func GetVolumes(ctx context.Context, server string, fs filters.Args) ([]*types.Volume, error) {
|
||||
cl, err := New(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*types.Volume, error) {
|
||||
volumeListOKBody, err := cl.VolumeList(ctx, fs)
|
||||
volumeList := volumeListOKBody.Volumes
|
||||
if err != nil {
|
||||
@ -32,12 +28,7 @@ func GetVolumeNames(volumes []*types.Volume) []string {
|
||||
return volumeNames
|
||||
}
|
||||
|
||||
func RemoveVolumes(ctx context.Context, server string, volumeNames []string, force bool) error {
|
||||
cl, err := New(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func RemoveVolumes(cl *client.Client, ctx context.Context, server string, volumeNames []string, force bool) error {
|
||||
for _, volName := range volumeNames {
|
||||
err := cl.VolumeRemove(ctx, volName, force)
|
||||
if err != nil {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
@ -368,16 +369,27 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
|
||||
}
|
||||
}
|
||||
|
||||
var bar *progressbar.ProgressBar
|
||||
for server := range servers {
|
||||
// validate that all server connections work
|
||||
if _, err := client.New(server); err != nil {
|
||||
return statuses, err
|
||||
}
|
||||
}
|
||||
|
||||
var bar *progressbar.ProgressBar
|
||||
if !MachineReadable {
|
||||
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
|
||||
}
|
||||
|
||||
ch := make(chan stack.StackStatus, len(servers))
|
||||
for server := range servers {
|
||||
cl, err := client.New(server)
|
||||
if err != nil {
|
||||
return statuses, err
|
||||
}
|
||||
|
||||
go func(s string) {
|
||||
ch <- stack.GetAllDeployedServices(s)
|
||||
ch <- stack.GetAllDeployedServices(cl, s)
|
||||
if !MachineReadable {
|
||||
bar.Add(1)
|
||||
}
|
||||
@ -386,6 +398,10 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
|
||||
|
||||
for range servers {
|
||||
status := <-ch
|
||||
if status.Err != nil {
|
||||
return statuses, status.Err
|
||||
}
|
||||
|
||||
for _, service := range status.Services {
|
||||
result := make(map[string]string)
|
||||
name := service.Spec.Labels[convert.LabelNamespace]
|
||||
|
@ -2,7 +2,6 @@ package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
dConfig "github.com/docker/cli/cli/config"
|
||||
@ -43,68 +42,3 @@ func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
|
||||
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
|
||||
return contextStore.New(dir, config)
|
||||
}
|
||||
|
||||
// missingContextMsg helps end-user debug missing docker context issues. This
|
||||
// version of the message has no app domain name included. This is due to the
|
||||
// code paths being unable to determine which app is requesting a server
|
||||
// connection at certain points. It is preferred to use
|
||||
// missingContextWithAppMsg where possible and only use missingContextMsg when
|
||||
// the call path is located too deep in the SSH stack.
|
||||
var missingContextMsg = `unable to find Docker context for %s?
|
||||
|
||||
Please run "abra server ls -p" to confirm. If you see "unknown" in the table
|
||||
output then you need to run the following command:
|
||||
|
||||
abra server add %s <args>
|
||||
|
||||
See "abra server add --help" for more.
|
||||
`
|
||||
|
||||
// missingContextWithAppMsg helps end-users debug missing docker context
|
||||
// issues. The app name is included in this message for extra clarity. See
|
||||
// missingContextMsg docs for alternative usage.
|
||||
var missingContextWithAppMsg = `unable to find Docker context for %s?
|
||||
|
||||
%s (app) is deployed on %s (server).
|
||||
|
||||
Please run "abra server ls -p" to confirm. If you see "unknown" in the table
|
||||
output then you need to run the following command:
|
||||
|
||||
abra server add %s <args>
|
||||
|
||||
See "abra server add --help" for more.
|
||||
`
|
||||
|
||||
// HasDockerContext figures out if a local setup has a working docker context
|
||||
// configuration or not. This usually tells us if they'll be able to make a SSH
|
||||
// connection to a server or not and can be a useful way to signal to end-users
|
||||
// that they need to fix something up if missing.
|
||||
func HasDockerContext(appName, serverName string) error {
|
||||
dockerContextStore := NewDefaultDockerContextStore()
|
||||
contexts, err := dockerContextStore.Store.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ctx := range contexts {
|
||||
if ctx.Name == serverName {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if appName != "" {
|
||||
return fmt.Errorf(
|
||||
missingContextWithAppMsg,
|
||||
serverName,
|
||||
appName,
|
||||
serverName,
|
||||
serverName,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Errorf(
|
||||
missingContextMsg,
|
||||
serverName,
|
||||
serverName,
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@ -32,35 +31,12 @@ func NewToken(provider, providerTokenEnvVar string) (string, error) {
|
||||
|
||||
// EnsureIPv4 ensures that an ipv4 address is set for a domain name
|
||||
func EnsureIPv4(domainName string) (string, error) {
|
||||
var ipv4 string
|
||||
|
||||
// comrade librehosters DNS resolver -> https://www.privacy-handbuch.de/handbuch_93d.htm
|
||||
freifunkDNS := "5.1.66.255:53"
|
||||
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: false,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: time.Millisecond * time.Duration(10000),
|
||||
}
|
||||
return d.DialContext(ctx, "udp", freifunkDNS)
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ips, err := resolver.LookupIPAddr(ctx, domainName)
|
||||
ipv4, err := net.ResolveIPAddr("ip", domainName)
|
||||
if err != nil {
|
||||
return ipv4, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return ipv4, fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
|
||||
}
|
||||
|
||||
ipv4 = ips[0].IP.To4().String()
|
||||
logrus.Debugf("%s points to %s (resolver: %s)", domainName, ipv4, freifunkDNS)
|
||||
|
||||
return ipv4, nil
|
||||
return ipv4.String(), nil
|
||||
}
|
||||
|
||||
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/decentral1se/passgen"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -117,7 +118,7 @@ func ParseSecretEnvVarValue(secret string) (secretValue, error) {
|
||||
}
|
||||
|
||||
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
||||
func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
|
||||
func GenerateSecrets(cl *dockerClient.Client, secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
|
||||
secrets := make(map[string]string)
|
||||
|
||||
var mutex sync.Mutex
|
||||
@ -146,7 +147,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil {
|
||||
if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
||||
ch <- nil
|
||||
@ -166,7 +167,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil {
|
||||
if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
||||
ch <- nil
|
||||
|
557
pkg/ssh/ssh.go
557
pkg/ssh/ssh.go
@ -1,37 +1,13 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"os/exec"
|
||||
"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/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
|
||||
@ -40,509 +16,66 @@ type HostConfig struct {
|
||||
User string
|
||||
}
|
||||
|
||||
// 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 nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
|
||||
// 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) (*Client, error) {
|
||||
var client *Client
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(domainName)
|
||||
if err != nil {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if sshAuth == "identity-file" {
|
||||
var err error
|
||||
client, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
} else {
|
||||
password := ""
|
||||
prompt := &survey.Password{
|
||||
Message: "SSH password?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &password); err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
var err error
|
||||
client, err = connectWithPasswordTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
password,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// sudoWriter supports sudo command handling
|
||||
type sudoWriter struct {
|
||||
b bytes.Buffer
|
||||
pw string
|
||||
stdin io.Writer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Write satisfies the write interface for sudoWriter
|
||||
func (w *sudoWriter) Write(p []byte) (int, error) {
|
||||
if strings.Contains(string(p), "sudo_password") {
|
||||
w.stdin.Write([]byte(w.pw + "\n"))
|
||||
w.pw = ""
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
return w.b.Write(p)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
sudoCmd := fmt.Sprintf("SSH_ASKPASS=/usr/bin/ssh-askpass; sudo -p sudo_password -S %s", cmd)
|
||||
|
||||
w := &sudoWriter{pw: passwd}
|
||||
w.stdin, err = session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Stdout = w
|
||||
session.Stderr = w
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
err = session.RequestPty("xterm", 80, 40, modes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := session.Run(sudoCmd); err != nil {
|
||||
return fmt.Errorf("%s", string(w.b.Bytes()))
|
||||
}
|
||||
|
||||
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", 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 you have already validated
|
||||
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, true)
|
||||
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) {
|
||||
logrus.Debugf("using ssh-agent to make an SSH connection for %s", host)
|
||||
|
||||
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agentCl := agent.NewClient(sshAgent)
|
||||
authMethod := ssh.PublicKeysCallback(agentCl.Signers)
|
||||
|
||||
loadedKeys, err := agentCl.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var convertedKeys []string
|
||||
for _, key := range loadedKeys {
|
||||
convertedKeys = append(convertedKeys, key.String())
|
||||
}
|
||||
|
||||
if len(convertedKeys) > 0 {
|
||||
logrus.Debugf("ssh-agent has these keys loaded: %s", strings.Join(convertedKeys, ","))
|
||||
} else {
|
||||
logrus.Debug("ssh-agent has no keys loaded")
|
||||
}
|
||||
|
||||
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 {
|
||||
if hostname == "default" || hostname == "local" {
|
||||
logrus.Debugf("not checking server SSH host key against local/default target")
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
// String presents a human friendly output for the HostConfig.
|
||||
func (h HostConfig) String() string {
|
||||
return fmt.Sprintf(
|
||||
"{host: %s, username: %s, port: %s, identityfile: %s}",
|
||||
h.Host,
|
||||
h.User,
|
||||
h.Port,
|
||||
h.IdentityFile,
|
||||
)
|
||||
|
||||
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, "", "", false)
|
||||
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, override bool) (HostConfig, error) {
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host using /usr/bin/ssh
|
||||
// directly. We therefore maintain consistent interop with this standard
|
||||
// tooling. This is useful because SSH confuses a lot of people and having to
|
||||
// learn how two tools (`ssh` and `abra`) handle SSH connection details instead
|
||||
// of one (just `ssh`) is Not Cool. Here's to less bug reports on this topic!
|
||||
func GetHostConfig(hostname string) (HostConfig, error) {
|
||||
var hostConfig HostConfig
|
||||
|
||||
if hostname == "" || override {
|
||||
if sshHost := ssh_config.Get(hostname, "Hostname"); sshHost != "" {
|
||||
hostname = sshHost
|
||||
}
|
||||
out, err := exec.Command("ssh", "-G", hostname).Output()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
|
||||
if username == "" || override {
|
||||
if sshUser := ssh_config.Get(hostname, "User"); sshUser != "" {
|
||||
username = sshUser
|
||||
} else {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
entries := strings.Split(line, " ")
|
||||
for idx, entry := range entries {
|
||||
if entry == "hostname" {
|
||||
hostConfig.Host = entries[idx+1]
|
||||
}
|
||||
username = systemUser.Username
|
||||
}
|
||||
}
|
||||
|
||||
if port == "" || override {
|
||||
if sshPort := ssh_config.Get(hostname, "Port"); sshPort != "" {
|
||||
// skip override probably correct port with dummy default value from
|
||||
// ssh_config which is 22. only when the original port number is empty
|
||||
// should we try this default. this might not cover all cases
|
||||
// unfortunately.
|
||||
if port != "" && sshPort != "22" {
|
||||
port = sshPort
|
||||
if entry == "user" {
|
||||
hostConfig.User = entries[idx+1]
|
||||
}
|
||||
if entry == "port" {
|
||||
hostConfig.Port = entries[idx+1]
|
||||
}
|
||||
if entry == "identityfile" {
|
||||
if hostConfig.IdentityFile == "" {
|
||||
hostConfig.IdentityFile = entries[idx+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if idf := ssh_config.Get(hostname, "IdentityFile"); idf != "" && idf != "~/.ssh/identity" {
|
||||
var err error
|
||||
idf, err = identityFileAbsPath(idf)
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
hostConfig.IdentityFile = idf
|
||||
} else {
|
||||
hostConfig.IdentityFile = ""
|
||||
}
|
||||
|
||||
hostConfig.Host = hostname
|
||||
hostConfig.Port = port
|
||||
hostConfig.User = username
|
||||
|
||||
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
|
||||
logrus.Debugf("retrieved ssh config for %s: %s", hostname, hostConfig.String())
|
||||
|
||||
return hostConfig, nil
|
||||
}
|
||||
|
||||
func identityFileAbsPath(relPath string) (string, error) {
|
||||
var err error
|
||||
var absPath string
|
||||
|
||||
if strings.HasPrefix(relPath, "~/") {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
return absPath, err
|
||||
}
|
||||
absPath = filepath.Join(systemUser.HomeDir, relPath[2:])
|
||||
// Fatal is a error output wrapper which aims to make SSH failures easier to
|
||||
// parse through re-wording.
|
||||
func Fatal(hostname string, err error) error {
|
||||
out := err.Error()
|
||||
if strings.Contains(out, "Host key verification failed.") {
|
||||
return fmt.Errorf("SSH host key verification failed for %s", hostname)
|
||||
} else if strings.Contains(out, "Could not resolve hostname") {
|
||||
return fmt.Errorf("could not resolve hostname for %s", hostname)
|
||||
} else if strings.Contains(out, "Connection timed out") {
|
||||
return fmt.Errorf("connection timed out for %s", hostname)
|
||||
} else {
|
||||
absPath, err = filepath.Abs(relPath)
|
||||
if err != nil {
|
||||
return absPath, err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("resolved %s to %s to read the ssh identity file", relPath, absPath)
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
|
@ -2,19 +2,15 @@ package commandconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
ctxPkg "coopcloud.tech/abra/pkg/context"
|
||||
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"
|
||||
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||
dClient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetConnectionHelper returns Docker-specific connection helper for the given URL.
|
||||
@ -37,29 +33,6 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
|
||||
if err := ctxPkg.HasDockerContext("", ctxConnDetails.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostConfig, err := sshPkg.GetHostConfig(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hostConfig.IdentityFile != "" {
|
||||
msg := "discovered %s as identity file for %s, using for ssh connection"
|
||||
logrus.Debugf(msg, hostConfig.IdentityFile, ctxConnDetails.Host)
|
||||
sshFlags = append(sshFlags, fmt.Sprintf("-o IdentityFile=%s", hostConfig.IdentityFile))
|
||||
}
|
||||
|
||||
return &connhelper.ConnectionHelper{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...)
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
abraClient "coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
@ -18,7 +17,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -57,20 +56,10 @@ func GetStackServices(ctx context.Context, dockerclient client.APIClient, namesp
|
||||
}
|
||||
|
||||
// GetDeployedServicesByLabel filters services by label
|
||||
func GetDeployedServicesByLabel(contextName string, label string) StackStatus {
|
||||
cl, err := abraClient.New(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
func GetDeployedServicesByLabel(cl *dockerClient.Client, contextName string, label string) StackStatus {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("label", label)
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters})
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
@ -78,18 +67,8 @@ func GetDeployedServicesByLabel(contextName string, label string) StackStatus {
|
||||
return StackStatus{services, nil}
|
||||
}
|
||||
|
||||
func GetAllDeployedServices(contextName string) StackStatus {
|
||||
cl, err := abraClient.New(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
func GetAllDeployedServices(cl *dockerClient.Client, contextName string) StackStatus {
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
@ -98,7 +77,7 @@ func GetAllDeployedServices(contextName string) StackStatus {
|
||||
}
|
||||
|
||||
// GetDeployedServicesByName filters services by name
|
||||
func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, stackName, serviceName string) StackStatus {
|
||||
func GetDeployedServicesByName(ctx context.Context, cl *dockerClient.Client, stackName, serviceName string) StackStatus {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", stackName, serviceName))
|
||||
|
||||
@ -111,7 +90,7 @@ func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, sta
|
||||
}
|
||||
|
||||
// IsDeployed chekcks whether an appp is deployed or not.
|
||||
func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string) (bool, string, error) {
|
||||
func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (bool, string, error) {
|
||||
version := "unknown"
|
||||
isDeployed := false
|
||||
|
||||
@ -142,7 +121,7 @@ func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string)
|
||||
}
|
||||
|
||||
// pruneServices removes services that are no longer referenced in the source
|
||||
func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
logrus.Infof("Failed to list services: %s\n", err)
|
||||
@ -158,9 +137,7 @@ func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace conve
|
||||
}
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
ctx := context.Background()
|
||||
|
||||
func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
if err := validateResolveImageFlag(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -170,7 +147,7 @@ func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config, a
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
|
||||
return deployCompose(ctx, cl, opts, cfg, appName, dontWait)
|
||||
return deployCompose(context.Background(), cl, opts, cfg, appName, dontWait)
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
@ -183,7 +160,7 @@ func validateResolveImageFlag(opts *Deploy) error {
|
||||
}
|
||||
}
|
||||
|
||||
func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.Prune {
|
||||
@ -241,7 +218,7 @@ func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) ma
|
||||
return serviceNetworks
|
||||
}
|
||||
|
||||
func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAPIClient, externalNetworks []string) error {
|
||||
func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAPIClient, externalNetworks []string) error {
|
||||
for _, networkName := range externalNetworks {
|
||||
if !container.NetworkMode(networkName).IsUserDefined() {
|
||||
// Networks that are not user defined always exist on all nodes as
|
||||
@ -250,7 +227,7 @@ func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAP
|
||||
}
|
||||
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
|
||||
switch {
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
|
||||
case err != nil:
|
||||
return err
|
||||
@ -261,7 +238,7 @@ func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAP
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm.SecretSpec) error {
|
||||
func createSecrets(ctx context.Context, cl *dockerClient.Client, secrets []swarm.SecretSpec) error {
|
||||
for _, secretSpec := range secrets {
|
||||
secret, _, err := cl.SecretInspectWithRaw(ctx, secretSpec.Name)
|
||||
switch {
|
||||
@ -270,7 +247,7 @@ func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm
|
||||
if err := cl.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
|
||||
}
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// secret does not exist, then we create a new one.
|
||||
logrus.Infof("Creating secret %s\n", secretSpec.Name)
|
||||
if _, err := cl.SecretCreate(ctx, secretSpec); err != nil {
|
||||
@ -283,7 +260,7 @@ func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm
|
||||
return nil
|
||||
}
|
||||
|
||||
func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm.ConfigSpec) error {
|
||||
func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm.ConfigSpec) error {
|
||||
for _, configSpec := range configs {
|
||||
config, _, err := cl.ConfigInspectWithRaw(ctx, configSpec.Name)
|
||||
switch {
|
||||
@ -292,7 +269,7 @@ func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm
|
||||
if err := cl.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
||||
}
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
logrus.Infof("Creating config %s\n", configSpec.Name)
|
||||
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
||||
@ -305,7 +282,7 @@ func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNetworks(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
|
||||
func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
|
||||
existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -335,7 +312,7 @@ func createNetworks(ctx context.Context, cl *dockerclient.Client, namespace conv
|
||||
|
||||
func deployServices(
|
||||
ctx context.Context,
|
||||
cl *dockerclient.Client,
|
||||
cl *dockerClient.Client,
|
||||
services map[string]swarm.ServiceSpec,
|
||||
namespace convert.Namespace,
|
||||
sendAuth bool,
|
||||
@ -469,7 +446,7 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
|
||||
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
|
||||
func WaitOnService(ctx context.Context, cl *dockerclient.Client, serviceID, appName string) error {
|
||||
func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appName string) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
|
Reference in New Issue
Block a user