Skip to content

Commit

Permalink
Merge pull request #154 from semaphoreci/debug-with-generated-key
Browse files Browse the repository at this point in the history
Debug sessions with Sempahore generated keys
  • Loading branch information
shiroyasha authored May 29, 2019
2 parents 92ec77e + 9cc7ae0 commit f9d513a
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 67 deletions.
4 changes: 2 additions & 2 deletions api/client/base_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ func (c *BaseClient) SetApiVersion(apiVersion string) *BaseClient {
return c
}

func (c *BaseClient) Get(kind string, name string) ([]byte, int, error) {
url := fmt.Sprintf("https://%s/api/%s/%s/%s", c.host, c.apiVersion, kind, name)
func (c *BaseClient) Get(kind string, resource string) ([]byte, int, error) {
url := fmt.Sprintf("https://%s/api/%s/%s/%s", c.host, c.apiVersion, kind, resource)

log.Printf("GET %s\n", url)

Expand Down
15 changes: 15 additions & 0 deletions api/client/jobs_v1_alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ func (c *JobsApiV1AlphaApi) GetJob(name string) (*models.JobV1Alpha, error) {
return models.NewJobV1AlphaFromJson(body)
}

func (c *JobsApiV1AlphaApi) GetJobDebugSSHKey(id string) (*models.JobDebugSSHKeyV1Alpha, error) {
path := fmt.Sprintf("%s/%s", id, "debug_ssh_key")
body, status, err := c.BaseClient.Get(c.ResourceNamePlural, path)

if err != nil {
return nil, errors.New(fmt.Sprintf("connecting to Semaphore failed '%s'", err))
}

if status != 200 {
return nil, errors.New(fmt.Sprintf("http status %d with message \"%s\" received from upstream", status, body))
}

return models.NewJobDebugSSHKeyV1AlphaFromJSON(body)
}

func (c *JobsApiV1AlphaApi) CreateJob(j *models.JobV1Alpha) (*models.JobV1Alpha, error) {
json_body, err := j.ToJson()

Expand Down
19 changes: 19 additions & 0 deletions api/models/job_debug_ssh_key_v1_alpha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package models

import "encoding/json"

type JobDebugSSHKeyV1Alpha struct {
Key string `json:"key,omitempty" yaml:"key"`
}

func NewJobDebugSSHKeyV1AlphaFromJSON(data []byte) (*JobDebugSSHKeyV1Alpha, error) {
key := JobDebugSSHKeyV1Alpha{}

err := json.Unmarshal(data, &key)

if err != nil {
return nil, err
}

return &key, nil
}
45 changes: 32 additions & 13 deletions api/models/job_v1_alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,39 @@ type JobV1AlphaSpecEnvVar struct {
Value string `json:"value,omitempty" yaml: "value,omitempty"`
}

type JobV1AlphaAgentImagePullSecret struct {
Name string `json:"name,omitempty" yaml: "name,omitempty"`
}

type JobV1AlphaAgentMachine struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"`
OsImage string `json:"os_image,omitempty" yaml:"os_image,omitempty"`
}

type JobV1AlphaAgentContainer struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Image string `json:"image,omitempty" yaml:"image,omitempty"`
Command string `json:"command,omitempty" yaml:"command,omitempty"`
EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml: "env_vars,omitempty"`
Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml: "secrets,omitempty"`
}

type JobV1AlphaAgent struct {
Machine JobV1AlphaAgentMachine `json:"machine,omitempty" yaml:"machine,omitempty"`
Containers []JobV1AlphaAgentContainer `json:"containers,omitempty" yaml:"containers,omitempty"`
ImagePullSecrets []JobV1AlphaAgentImagePullSecret `json:"image_pull_secrets,omitempty" yaml: "image_pull_secrets,omitempty"`
}

type JobV1AlphaSpec struct {
Agent struct {
Machine struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"`
OsImage string `json:"os_image,omitempty" yaml:"os_image,omitempty"`
} `json:"machine,omitempty" yaml:"machine,omitempty"`
} `json:"agent,omitempty" yaml:"agent,omitempty"`

Files []JobV1AlphaSpecFile `json:"files,omitempty" yaml: "files,omitempty"`
EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml: "env_vars,omitempty"`
Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml: "secrets,omitempty"`
Commands []string `json:"commands,omitempty" yaml:"commands,omitempty"`
EpilogueCommands []string `json:"epilogue_commands,omitempty" yaml:"epilogue_commands,omitempty"`
ProjectId string `json:"project_id,omitempty" yaml:"project_id,omitempty"`
Agent JobV1AlphaAgent `json:"agent,omitempty" yaml:"agent,omitempty"`
Files []JobV1AlphaSpecFile `json:"files,omitempty" yaml: "files,omitempty"`
EnvVars []JobV1AlphaSpecEnvVar `json:"env_vars,omitempty" yaml: "env_vars,omitempty"`
Secrets []JobV1AlphaSpecSecret `json:"secrets,omitempty" yaml: "secrets,omitempty"`
Commands []string `json:"commands,omitempty" yaml:"commands,omitempty"`
EpilogueAlwaysCommands []string `json:"epilogue_always_commands,omitempty" yaml:"epilogue_always_commands,omitempty"`
EpilogueOnPassCommands []string `json:"epilogue_on_pass_commands,omitempty" yaml:"epilogue_on_pass_commands,omitempty"`
EpilogueOnFailCommands []string `json:"epilogue_on_fail_commands,omitempty" yaml:"epilogue_on_fail_commands,omitempty"`
ProjectId string `json:"project_id,omitempty" yaml:"project_id,omitempty"`
}

type JobV1AlphaStatus struct {
Expand Down
6 changes: 5 additions & 1 deletion cmd/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ var attachCmd = &cobra.Command{
os.Exit(1)
}

conn, err := ssh.NewConnectionForJob(job)
// Get SSH key for job
sshKey, err := c.GetJobDebugSSHKey(job.Metadata.Id)
utils.Check(err)

conn, err := ssh.NewConnectionForJob(job, sshKey.Key)
utils.Check(err)
defer conn.Close()

conn.Session()
},
Expand Down
22 changes: 13 additions & 9 deletions cmd/debug_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

client "github.com/semaphoreci/cli/api/client"
models "github.com/semaphoreci/cli/api/models"
config "github.com/semaphoreci/cli/config"

"github.com/semaphoreci/cli/cmd/ssh"
"github.com/semaphoreci/cli/cmd/utils"
Expand All @@ -34,9 +33,6 @@ func NewDebugJobCmd() *cobra.Command {
}

func RunDebugJobCmd(cmd *cobra.Command, args []string) {
publicKey, err := config.GetPublicSshKeyForDebugSession()
utils.Check(err)

duration, err := cmd.Flags().GetDuration("duration")
utils.Check(err)

Expand All @@ -52,12 +48,20 @@ func RunDebugJobCmd(cmd *cobra.Command, args []string) {

// Copy everything to new job, except commands
job.Spec = oldJob.Spec
job.Spec.EpilogueCommands = []string{}
job.Spec.EpilogueAlwaysCommands = []string{}
job.Spec.EpilogueOnPassCommands = []string{}
job.Spec.EpilogueOnFailCommands = []string{}

// Construct a commands file and inject into job
commandsFileContent := fmt.Sprintf("%s\n%s",
commandsFileContent := strings.Join([]string{
strings.Join(oldJob.Spec.Commands, "\n"),
strings.Join(oldJob.Spec.EpilogueCommands, "\n"))
"# epilogue always commands",
strings.Join(oldJob.Spec.EpilogueAlwaysCommands, "\n"),
"# epilogue on pass commands",
strings.Join(oldJob.Spec.EpilogueOnPassCommands, "\n"),
"# epilogue on fail commands",
strings.Join(oldJob.Spec.EpilogueOnFailCommands, "\n"),
}, "\n")

job.Spec.Files = []models.JobV1AlphaSpecFile{
models.JobV1AlphaSpecFile{
Expand All @@ -66,9 +70,9 @@ func RunDebugJobCmd(cmd *cobra.Command, args []string) {
},
}

// Overwrite commands with a simple SSH public key insertion and infinite sleep
// Overwrite commands with a sleep. This will keep the job up for N seconds.
// Original commands are inserted into commands.sh.
job.Spec.Commands = []string{
fmt.Sprintf("echo '%s' >> .ssh/authorized_keys", publicKey),
fmt.Sprintf("sleep %d", int(duration.Seconds())),
}

Expand Down
5 changes: 0 additions & 5 deletions cmd/debug_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
models "github.com/semaphoreci/cli/api/models"
"github.com/semaphoreci/cli/cmd/ssh"
"github.com/semaphoreci/cli/cmd/utils"
"github.com/semaphoreci/cli/config"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -36,9 +35,6 @@ func NewDebugProjectCmd() *cobra.Command {
}

func RunDebugProjectCmd(cmd *cobra.Command, args []string) {
publicKey, err := config.GetPublicSshKeyForDebugSession()
utils.Check(err)

machineType, err := cmd.Flags().GetString("machine-type")
utils.Check(err)

Expand All @@ -61,7 +57,6 @@ func RunDebugProjectCmd(cmd *cobra.Command, args []string) {
job.Spec.ProjectId = project.Metadata.Id

job.Spec.Commands = []string{
fmt.Sprintf("echo '%s' >> .ssh/authorized_keys", publicKey),
fmt.Sprintf("sleep %d", int(duration.Seconds())),
}

Expand Down
45 changes: 35 additions & 10 deletions cmd/ssh/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package ssh

import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
Expand All @@ -11,20 +13,33 @@ import (
)

type Connection struct {
IP string
Port int32
Username string
IP string
Port int32
Username string
SSHKeyFile *os.File
}

func NewConnection(ip string, port int32, username string) (*Connection, error) {
func NewConnection(ip string, port int32, username string, sshKey string) (*Connection, error) {
sshKeyFile, err := ioutil.TempFile("", "sem-cli-debug-private-key")
if err != nil {
return nil, err
}
if _, err := sshKeyFile.Write([]byte(sshKey)); err != nil {
return nil, err
}
if err := sshKeyFile.Close(); err != nil {
return nil, err
}

return &Connection{
IP: ip,
Port: port,
Username: username,
IP: ip,
Port: port,
Username: username,
SSHKeyFile: sshKeyFile,
}, nil
}

func NewConnectionForJob(job *models.JobV1Alpha) (*Connection, error) {
func NewConnectionForJob(job *models.JobV1Alpha, sshKeyPath string) (*Connection, error) {
ip := job.Status.Agent.Ip

var port int32
Expand All @@ -42,7 +57,11 @@ func NewConnectionForJob(job *models.JobV1Alpha) (*Connection, error) {
return nil, err
}

return NewConnection(ip, port, "semaphore")
return NewConnection(ip, port, "semaphore", sshKeyPath)
}

func (c *Connection) Close() {
os.Remove(c.SSHKeyFile.Name()) // clean up
}

func (c *Connection) WaitUntilReady(attempts int, callback func()) error {
Expand All @@ -66,7 +85,10 @@ func (c *Connection) WaitUntilReady(attempts int, callback func()) error {

func (c *Connection) IsReady() (bool, error) {
cmd, err := c.sshCommand("echo 'success'", false)
log.Printf("SSH connection: Running %+v", cmd)

output, err := cmd.CombinedOutput()
log.Printf("SSH connection: Output %s", output)

if err == nil && strings.Contains(string(output), "success") {
return true, nil
Expand All @@ -86,7 +108,7 @@ func (c *Connection) IsReady() (bool, error) {
}

func (c *Connection) Session() error {
cmd, err := c.sshCommand("bash --login", true)
cmd, err := c.sshCommand("bash /tmp/ssh_jump_point", true)

if err != nil {
return err
Expand Down Expand Up @@ -121,13 +143,16 @@ func (c *Connection) sshCommand(directive string, interactive bool) (*exec.Cmd,
interactiveFlag = "-T"
}

sshKeyFlag := fmt.Sprintf("-i%s", c.SSHKeyFile.Name())

noStrictFlag := "-oStrictHostKeyChecking=no"
timeoutFlag := "-oConnectTimeout=5"
userAndIp := fmt.Sprintf("%s@%s", c.Username, c.IP)

cmd := exec.Command(
path,
interactiveFlag,
sshKeyFlag,
portFlag,
timeoutFlag,
noStrictFlag,
Expand Down
10 changes: 9 additions & 1 deletion cmd/ssh/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ func StartDebugSession(job *models.JobV1Alpha, message string) error {
return err
}

conn, err := NewConnectionForJob(job)
// Get SSH key for job
sshKey, err := c.GetJobDebugSSHKey(job.Metadata.Id)
if err != nil {
fmt.Printf("\n[ERROR] %s\n", err)
return err
}

conn, err := NewConnectionForJob(job, sshKey.Key)
if err != nil {
fmt.Printf("\n[ERROR] %s\n", err)
return err
}
defer conn.Close()

fmt.Printf("* Waiting for ssh daemon to become ready .")
err = conn.WaitUntilReady(20, func() { fmt.Printf(".") })
fmt.Println()
Expand Down
26 changes: 0 additions & 26 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,32 +76,6 @@ func SetAuth(token string) {
Set(key_path, token)
}

const unsetPublicSshKeyMsg = `Before creating a debug session job, configure the debug.PublicSshKey value.
Examples:
# Configuring public ssh key with a literal
sem config set debug.PublicSshKey "ssh-rsa AX3....DD"
# Configuring public ssh key with a file
sem config set debug.PublicSshKey "$(cat ~/.ssh/id_rsa.pub)"
# Configuring public ssh key with your GitHub keys
sem config set debug.PublicSshKey "$(curl -s https://github.com/<username>.keys)"
`

func GetPublicSshKeyForDebugSession() (string, error) {
publicKey := viper.GetString("debug.PublicSshKey")

if publicKey == "" {
err := fmt.Errorf("Public SSH key for debugging is not configured.\n\n%s", unsetPublicSshKeyMsg)

return "", err
}

return publicKey, nil
}

func GetHost() string {
if flag.Lookup("test.v") == nil {
context := GetActiveContext()
Expand Down

0 comments on commit f9d513a

Please sign in to comment.