Skip to content

Commit 954e6dd

Browse files
committed
feat: add ability to follow build logs after deploy
1 parent 4b3b65a commit 954e6dd

File tree

3 files changed

+144
-3
lines changed

3 files changed

+144
-3
lines changed

cmd/deploy.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strconv"
8+
"time"
79

10+
"github.com/logrusorgru/aurora"
811
"github.com/uselagoon/lagoon-cli/pkg/output"
912

1013
lclient "github.com/uselagoon/machinery/api/lagoon/client"
1114

1215
"github.com/spf13/cobra"
16+
lagoonssh "github.com/uselagoon/lagoon-cli/pkg/lagoon/ssh"
1317
"github.com/uselagoon/machinery/api/lagoon"
1418
"github.com/uselagoon/machinery/api/schema"
1519
)
@@ -47,6 +51,10 @@ use 'lagoon deploy latest' instead`,
4751
if err != nil {
4852
return err
4953
}
54+
follow, err := cmd.Flags().GetBool("follow")
55+
if err != nil {
56+
return err
57+
}
5058
if err := requiredInputCheck("Project name", cmdProjectName, "Branch name", branch); err != nil {
5159
return err
5260
}
@@ -85,6 +93,10 @@ use 'lagoon deploy latest' instead`,
8593
resultData := output.Result{Result: result.DeployEnvironmentBranch}
8694
r := output.RenderResult(resultData, outputOptions)
8795
fmt.Fprintf(cmd.OutOrStdout(), "%s", r)
96+
97+
if follow {
98+
return followDeployLogs(cmd, cmdProjectName, branch, resultData.Result, debug)
99+
}
88100
}
89101
return nil
90102
},
@@ -115,6 +127,10 @@ var deployPromoteCmd = &cobra.Command{
115127
if err != nil {
116128
return err
117129
}
130+
follow, err := cmd.Flags().GetBool("follow")
131+
if err != nil {
132+
return err
133+
}
118134
if err := requiredInputCheck("Project name", cmdProjectName, "Source environment", sourceEnvironment, "Destination environment", destinationEnvironment); err != nil {
119135
return err
120136
}
@@ -150,6 +166,10 @@ var deployPromoteCmd = &cobra.Command{
150166
resultData := output.Result{Result: result.DeployEnvironmentPromote}
151167
r := output.RenderResult(resultData, outputOptions)
152168
fmt.Fprintf(cmd.OutOrStdout(), "%s", r)
169+
170+
if follow {
171+
return followDeployLogs(cmd, cmdProjectName, destinationEnvironment, resultData.Result, debug)
172+
}
153173
}
154174
return nil
155175
},
@@ -175,6 +195,10 @@ This environment should already exist in lagoon. It is analogous with the 'Deplo
175195
if err != nil {
176196
return err
177197
}
198+
follow, err := cmd.Flags().GetBool("follow")
199+
if err != nil {
200+
return err
201+
}
178202

179203
buildVarStrings, err := cmd.Flags().GetStringArray("buildvar")
180204
if err != nil {
@@ -213,6 +237,10 @@ This environment should already exist in lagoon. It is analogous with the 'Deplo
213237
resultData := output.Result{Result: result.DeployEnvironmentLatest}
214238
r := output.RenderResult(resultData, outputOptions)
215239
fmt.Fprintf(cmd.OutOrStdout(), "%s", r)
240+
241+
if follow {
242+
return followDeployLogs(cmd, cmdProjectName, cmdProjectEnvironment, resultData.Result, debug)
243+
}
216244
}
217245
return nil
218246
},
@@ -273,6 +301,11 @@ This pullrequest may not already exist as an environment in lagoon.`,
273301
if err != nil {
274302
return err
275303
}
304+
follow, err := cmd.Flags().GetBool("follow")
305+
if err != nil {
306+
return err
307+
}
308+
276309
if yesNo(fmt.Sprintf("You are attempting to deploy pull request '%v' for project '%s', are you sure?", prNumber, cmdProjectName)) {
277310
current := lagoonCLIConfig.Current
278311
token := lagoonCLIConfig.Lagoons[current].Token
@@ -302,6 +335,10 @@ This pullrequest may not already exist as an environment in lagoon.`,
302335
resultData := output.Result{Result: result.DeployEnvironmentPullrequest}
303336
r := output.RenderResult(resultData, outputOptions)
304337
fmt.Fprintf(cmd.OutOrStdout(), "%s", r)
338+
339+
if follow {
340+
return followDeployLogs(cmd, cmdProjectName, fmt.Sprintf("pr-%d", prNumber), resultData.Result, debug)
341+
}
305342
}
306343
return nil
307344
},
@@ -315,16 +352,19 @@ func init() {
315352

316353
const returnDataUsageText = "Returns the build name instead of success text"
317354
deployLatestCmd.Flags().Bool("returndata", false, returnDataUsageText)
355+
deployLatestCmd.Flags().Bool("follow", false, "Follow the deploy logs")
318356
deployLatestCmd.Flags().StringArray("buildvar", []string{}, "Add one or more build variables to deployment (--buildvar KEY1=VALUE1 [--buildvar KEY2=VALUE2])")
319357

320358
deployBranchCmd.Flags().StringP("branch", "b", "", "Branch name to deploy")
321359
deployBranchCmd.Flags().StringP("branch-ref", "r", "", "Branch ref to deploy")
322360
deployBranchCmd.Flags().Bool("returndata", false, returnDataUsageText)
361+
deployBranchCmd.Flags().Bool("follow", false, "Follow the deploy logs")
323362
deployBranchCmd.Flags().StringArray("buildvar", []string{}, "Add one or more build variables to deployment (--buildvar KEY1=VALUE1 [--buildvar KEY2=VALUE2])")
324363

325364
deployPromoteCmd.Flags().StringP("destination", "d", "", "Destination environment name to create")
326365
deployPromoteCmd.Flags().StringP("source", "s", "", "Source environment name to use as the base to deploy from")
327366
deployPromoteCmd.Flags().Bool("returndata", false, returnDataUsageText)
367+
deployPromoteCmd.Flags().Bool("follow", false, "Follow the deploy logs")
328368
deployPromoteCmd.Flags().StringArray("buildvar", []string{}, "Add one or more build variables to deployment (--buildvar KEY1=VALUE1 [--buildvar KEY2=VALUE2])")
329369

330370
deployPullrequestCmd.Flags().StringP("title", "t", "", "Pullrequest title")
@@ -334,5 +374,87 @@ func init() {
334374
deployPullrequestCmd.Flags().StringP("head-branch-name", "H", "", "Pullrequest head branch name")
335375
deployPullrequestCmd.Flags().StringP("head-branch-ref", "M", "", "Pullrequest head branch reference hash")
336376
deployPullrequestCmd.Flags().Bool("returndata", false, returnDataUsageText)
377+
deployPullrequestCmd.Flags().Bool("follow", false, "Follow the deploy logs")
337378
deployPullrequestCmd.Flags().StringArray("buildvar", []string{}, "Add one or more build variables to deployment (--buildvar KEY1=VALUE1 [--buildvar KEY2=VALUE2])")
338379
}
380+
381+
func followDeployLogs(
382+
cmd *cobra.Command,
383+
projectName,
384+
environmentName,
385+
buildName string,
386+
debug bool,
387+
) error {
388+
safeEnvName := makeSafe(shortenEnvironment(projectName, environmentName))
389+
sshHost, sshPort, username, _, err := getSSHHostPort(safeEnvName, debug)
390+
if err != nil {
391+
return fmt.Errorf("couldn't get SSH endpoint: %v", err)
392+
}
393+
ignoreHostKey, acceptNewHostKey :=
394+
lagoonssh.CheckStrictHostKey(strictHostKeyCheck)
395+
sshConfig, closeSSHAgent, err := getSSHClientConfig(
396+
username,
397+
fmt.Sprintf("%s:%s", sshHost, sshPort),
398+
ignoreHostKey,
399+
acceptNewHostKey)
400+
if err != nil {
401+
return fmt.Errorf("couldn't get SSH client config: %v", err)
402+
}
403+
defer func() {
404+
err = closeSSHAgent()
405+
if err != nil {
406+
fmt.Fprintf(os.Stderr, "error closing ssh agent:%v\n", err)
407+
}
408+
}()
409+
ctx, cancel := context.WithCancel(context.Background())
410+
defer cancel()
411+
// start background ticker to close session when deploy completes
412+
ticker := time.NewTicker(10 * time.Second)
413+
defer ticker.Stop()
414+
go func() {
415+
defer cancel()
416+
for {
417+
select {
418+
case <-ctx.Done():
419+
return
420+
case <-ticker.C:
421+
validateToken(lagoonCLIConfig.Current)
422+
current := lagoonCLIConfig.Current
423+
token := lagoonCLIConfig.Lagoons[current].Token
424+
lc := lclient.New(
425+
lagoonCLIConfig.Lagoons[current].GraphQL,
426+
lagoonCLIVersion,
427+
lagoonCLIConfig.Lagoons[current].Version,
428+
&token,
429+
debug)
430+
// ignore errors here since we can't really do anything about them
431+
deployment, _ := lagoon.GetDeploymentByName(
432+
ctx, cmdProjectName, cmdProjectEnvironment, buildName, false, lc)
433+
if deployment.Completed != "" && deployment.Status != "running" {
434+
var status string
435+
switch deployment.Status {
436+
case "complete":
437+
status = "complete ✅"
438+
case "failed":
439+
status = "failed ❌"
440+
case "cancelled":
441+
status = "cancelled 🛑"
442+
default:
443+
status = deployment.Status
444+
}
445+
fmt.Fprintf(
446+
cmd.OutOrStdout(),
447+
"Deployment %s finished with status: %s\n",
448+
aurora.Yellow(buildName),
449+
status)
450+
return
451+
}
452+
}
453+
}
454+
}()
455+
fmt.Fprintf(cmd.OutOrStdout(), "Streaming deploy logs...\n")
456+
return lagoonssh.LogStream(ctx, sshConfig, sshHost, sshPort, []string{
457+
"lagoonSystem=build",
458+
"logs=tailLines=32,follow",
459+
})
460+
}

cmd/logs.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,10 @@ var logsCmd = &cobra.Command{
215215
fmt.Fprintf(os.Stderr, "error closing ssh agent:%v\n", err)
216216
}
217217
}()
218+
ctx, cancel := context.WithCancel(context.TODO())
219+
defer cancel()
218220
// start SSH log streaming session
219-
err = lagoonssh.LogStream(sshConfig, sshHost, sshPort, argv)
221+
err = lagoonssh.LogStream(ctx, sshConfig, sshHost, sshPort, argv)
220222
if err != nil {
221223
output.RenderError(err.Error(), outputOptions)
222224
switch e := err.(type) {

pkg/lagoon/ssh/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package ssh
44
import (
55
"bufio"
66
"bytes"
7+
"context"
78
"fmt"
89
"net"
910
"os"
@@ -18,7 +19,13 @@ import (
1819
// LogStream connects to host:port using the given config, and executes the
1920
// argv command. It does not request a PTY, and instead just streams the
2021
// response to the attached terminal. argv should contain a logs=... argument.
21-
func LogStream(config *ssh.ClientConfig, host, port string, argv []string) error {
22+
func LogStream(
23+
ctx context.Context,
24+
config *ssh.ClientConfig,
25+
host,
26+
port string,
27+
argv []string,
28+
) error {
2229
// https://stackoverflow.com/a/37088088
2330
client, err := ssh.Dial("tcp", host+":"+port, config)
2431
if err != nil {
@@ -29,14 +36,24 @@ func LogStream(config *ssh.ClientConfig, host, port string, argv []string) error
2936
return fmt.Errorf("couldn't create SSH session: %v", err)
3037
}
3138
defer session.Close()
39+
// close the session when the context is cancelled
40+
go func() {
41+
<-ctx.Done()
42+
session.Close()
43+
}()
3244
session.Stdout = os.Stdout
3345
session.Stderr = os.Stderr
3446
session.Stdin = os.Stdin
3547
err = session.Start(strings.Join(argv, " "))
3648
if err != nil {
3749
return fmt.Errorf("couldn't start SSH session: %v", err)
3850
}
39-
return session.Wait()
51+
err = session.Wait()
52+
if ctx.Err() == nil {
53+
// context not done, so return session.Wait() error
54+
return err
55+
}
56+
return nil
4057
}
4158

4259
// InteractiveSSH .

0 commit comments

Comments
 (0)