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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions pkg/ckecli/cmd/scp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"

"github.com/cybozu-go/log"
"github.com/cybozu-go/well"
"github.com/spf13/cobra"
)
Expand All @@ -28,19 +28,51 @@ func detectSCPNode(args []string) (string, error) {
return nodeName, nil
}

func scp(ctx context.Context, args []string) error {
func scpSubMain(ctx context.Context, args []string) error {
pipeFilename, err := createFifo()
if err != nil {
log.Error("failed to create named pipe", map[string]interface{}{
log.FnError: err,
})
return err
}
defer os.Remove(pipeFilename)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to defer here.

Suggested change
defer os.Remove(pipeFilename)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed it

node, err := detectSCPNode(args)
if err != nil {
log.Error("failed to find the node name for scp", map[string]interface{}{
log.FnError: err,
})
return err
}
fifo, err := sshPrivateKey(node)

pirvateKey, err := getPrivateKey(node)
if err != nil {
log.Error("failed to get the private key for scp", map[string]interface{}{
log.FnError: err,
})
return err
}

go func() {
if _, err := startSshAgent(ctx, pipeFilename); err != nil {
log.Error("failed to start ssh-agent for scp", map[string]interface{}{
log.FnError: err,
"node": node,
})
}
}()
defer killSshAgent(ctx)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to defer here.

Suggested change
defer killSshAgent(ctx)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put it here.

if err = writeToFifo(pipeFilename, pirvateKey); err != nil {
log.Error("failed to write the named pipe", map[string]interface{}{
log.FnError: err,
"pipe": pipeFilename,
})
return err
}
defer os.Remove(fifo)

scpArgs := []string{
"-i", fifo,
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=60",
Expand All @@ -50,8 +82,6 @@ func scp(ctx context.Context, args []string) error {
}

scpArgs = append(scpArgs, args...)

fmt.Println(scpArgs)
c := exec.CommandContext(ctx, "scp", scpArgs...)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
Expand All @@ -75,7 +105,7 @@ NODE is IP address or hostname of the node.
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
well.Go(func(ctx context.Context) error {
return scp(ctx, args)
return scpSubMain(ctx, args)
})
well.Stop()
return well.Wait()
Expand Down
166 changes: 129 additions & 37 deletions pkg/ckecli/cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"strconv"
"strings"
"syscall"
"time"

"github.com/cybozu-go/cke"
"github.com/cybozu-go/log"
Expand All @@ -26,54 +25,49 @@ func detectSSHNode(arg string) string {
return nodeName
}

func writeToFifo(fifo string, data string) {
f, err := os.OpenFile(fifo, os.O_WRONLY, 0600)
func createFifo() (string, error) {
usr, err := user.Current()
if err != nil {
log.Error("failed to open fifo", map[string]interface{}{
log.FnError: err,
"fifo": fifo,
})
return
return "", err
}
defer f.Close()

_, err = f.WriteString(data)
if err != nil {
log.Error("failed to write to fifo", map[string]interface{}{
log.FnError: err,
"fifo": fifo,
})
fifoFilePath := filepath.Join(usr.HomeDir, ".ssh", "ckecli-ssh-key-"+strconv.Itoa(os.Getpid()))
_, err = os.Stat(fifoFilePath)
if os.IsExist(err) {
return fifoFilePath, nil
}
}

func sshPrivateKey(nodeName string) (string, error) {
usr, err := user.Current()
if err != nil {
if !os.IsNotExist(err) {
return "", err
}

err = os.MkdirAll(filepath.Join(usr.HomeDir, ".ssh"), 0700)
if err != nil {
return "", err
}
fifo := filepath.Join(usr.HomeDir, ".ssh", "ckecli-ssh-key-"+strconv.Itoa(os.Getpid()))
err = syscall.Mkfifo(fifo, 0600)

err = syscall.Mkfifo(fifoFilePath, 0600)
if err != nil {
return "", err
}

return fifoFilePath, err
}

func getPrivateKey(nodeName string) (string, error) {
vc, err := inf.Vault()
if err != nil {
return "", err
}

secret, err := vc.Logical().Read(cke.SSHSecret)
if err != nil {
return "", err
}
if secret == nil {
return "", errors.New("no ssh private keys")
}
privKeys := secret.Data

privKeys := secret.Data
mykey, ok := privKeys[nodeName]
if !ok {
mykey = privKeys[""]
Expand All @@ -82,28 +76,126 @@ func sshPrivateKey(nodeName string) (string, error) {
return "", errors.New("no ssh private key for " + nodeName)
}

go func() {
// OpenSSH reads the private key file three times, it need to write key three times.
writeToFifo(fifo, mykey.(string))
time.Sleep(100 * time.Millisecond)
writeToFifo(fifo, mykey.(string))
time.Sleep(100 * time.Millisecond)
writeToFifo(fifo, mykey.(string))
}()
return mykey.(string), nil
}

func startSshAgent(ctx context.Context, privateKeyFile string) (map[string]string, error) {
myEnv := make(map[string]string)

cmd := exec.CommandContext(ctx, "ssh-agent", "-s")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}

// Set enviromental variable to communicate ssh-agent
line := strings.Split(string(stdoutStderr), "\n")
partOfLine := strings.Split(line[0], ";")
kvPair1 := strings.Split(partOfLine[0], "=")
myEnv[kvPair1[0]] = kvPair1[1]
err = os.Setenv(kvPair1[0], kvPair1[1])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the cmd.Env.

There is no need to reflect SSH_AUTH_SOCK and SSH_AGENT_PID in the ckecli process.
These environment variables should only be set for child commands.

if err != nil {
log.Error("failed to set environment variable 1", map[string]interface{}{
log.FnError: err,
"Env": kvPair1[0],
"Val": kvPair1[1],
})
return nil, err
}
partOfLine = strings.Split(line[1], ";")
kvPair2 := strings.Split(partOfLine[0], "=")
myEnv[kvPair2[0]] = kvPair2[1]
err = os.Setenv(kvPair2[0], kvPair2[1])
if err != nil {
log.Error("failed to set environment variable 2", map[string]interface{}{
log.FnError: err,
"Env": kvPair2[0],
"Val": kvPair2[1],
})
return nil, err
}

return fifo, nil
cmd = exec.CommandContext(ctx, "ssh-add", privateKeyFile)
_, err = cmd.CombinedOutput()
if err != nil {
log.Error("failed to add the private key", map[string]interface{}{
log.FnError: err,
})
return nil, err
}
log.Info("Successfuly added the private key", map[string]interface{}{
"Env": kvPair1[0],
"Val": kvPair1[1],
})

return myEnv, nil
}

func killSshAgent(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "ssh-agent", "-k")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
log.Error("failed to run ssh-agent -k", map[string]interface{}{
log.FnError: err,
})
return err
}
log.Debug("killed ssh-agent", map[string]interface{}{
"stdout_stderr": string(stdoutStderr),
})
return nil
}

func ssh(ctx context.Context, args []string) error {
func writeToFifo(fifo string, data string) error {
f, err := os.OpenFile(fifo, os.O_WRONLY|os.O_APPEND, os.ModeNamedPipe)
if err != nil {
return err
}
defer f.Close()
if _, err = f.Write([]byte(data)); err != nil {
return err
}
return nil
}

func sshSubMain(ctx context.Context, args []string) error {
pipeFilename, err := createFifo()
if err != nil {
log.Error("failed to create the named pipe", map[string]interface{}{
log.FnError: err,
})
return err
}
defer os.Remove(pipeFilename)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to defer here. If an error occurs until line 205, the file may remain.

defer os.Remove(fifo)

Copy link
Contributor Author

@takara9 takara9 Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved "defer os.Remove(pipeFilename)" after the error check.

node := detectSSHNode(args[0])
fifo, err := sshPrivateKey(node)
pirvateKey, err := getPrivateKey(node)
if err != nil {
log.Error("failed to get the private key for ssh", map[string]interface{}{
log.FnError: err,
})
return err
}

go func() {
if _, err := startSshAgent(ctx, pipeFilename); err != nil {
log.Error("failed to start ssh-agent for ssh", map[string]interface{}{
log.FnError: err,
"node": node,
})
}
}()
defer killSshAgent(ctx)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If writeToFifo() fails, ssh-agent is not killed.

Suggested change
defer killSshAgent(ctx)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved it.

if err = writeToFifo(pipeFilename, pirvateKey); err != nil {
log.Error("failed to write the named pipe", map[string]interface{}{
log.FnError: err,
"pipe": pipeFilename,
})
return err
}
defer os.Remove(fifo)

sshArgs := []string{
"-i", fifo,
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=60",
Expand All @@ -130,7 +222,7 @@ If COMMAND is specified, it will be executed on the node.
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
well.Go(func(ctx context.Context) error {
return ssh(ctx, args)
return sshSubMain(ctx, args)
})
well.Stop()
return well.Wait()
Expand Down