Skip to content

Commit

Permalink
Merge pull request #16 from uselagoon/spinner
Browse files Browse the repository at this point in the history
feat: show a spinner during ssh connection
  • Loading branch information
smlx authored Feb 7, 2022
2 parents 95ff664 + d98c368 commit 5bfb5f6
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 11 deletions.
6 changes: 4 additions & 2 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ builds:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
goos:
- linux
- id: ssh-portal
dir: cmd/ssh-portal
binary: ssh-portal
Expand All @@ -22,4 +23,5 @@ builds:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
goos:
- linux
35 changes: 26 additions & 9 deletions internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"k8s.io/client-go/tools/remotecommand"
)

var (
const (
timeout = 90 * time.Second
projectIDLabel = "lagoon.sh/projectId"
environmentIDLabel = "lagoon.sh/environmentId"
Expand Down Expand Up @@ -140,20 +140,29 @@ func (c *Client) ensureScaled(ctx context.Context, deployment, namespace string)
c.hasRunningPod(ctx, deployment, namespace))
}

// Exec joins the given streams to the command or, if command is empty, to a
// shell running in the given pod.
func (c *Client) Exec(ctx context.Context, deployment, namespace string,
command []string, stdio io.ReadWriter, stderr io.Writer, tty bool) error {
// getExecutor prepares the environment by ensuring pods are scaled etc. and
// returns an executor object.
func (c *Client) getExecutor(ctx context.Context, deployment, namespace string,
command []string, stdio io.ReadWriter, stderr io.Writer, tty bool) (remotecommand.Executor, error) {
// If there's a tty, then animate a spinner if this function takes too long
// to return.
// Defer context cancel() after wg.Wait() because we need the context to
// cancel first in order to shortcut spinAfter() and avoid a spinner if shell
// acquisition is fast enough.
ctx, cancel := context.WithTimeout(ctx, timeout)
if tty {
wg := spinAfter(ctx, stderr, 2*time.Second)
defer wg.Wait()
}
defer cancel()
// ensure the deployment has at least one replica
if err := c.ensureScaled(ctx, deployment, namespace); err != nil {
return fmt.Errorf("couldn't scale deployment: %v", err)
return nil, fmt.Errorf("couldn't scale deployment: %v", err)
}
// get the name of the first pod in the deployment
podName, err := c.podName(ctx, deployment, namespace)
if err != nil {
return fmt.Errorf("couldn't get pod name: %v", err)
return nil, fmt.Errorf("couldn't get pod name: %v", err)
}
// check the command. if there isn't one, give the user a shell.
if len(command) == 0 {
Expand All @@ -173,9 +182,17 @@ func (c *Client) Exec(ctx context.Context, deployment, namespace string,
scheme.ParameterCodec,
)
// construct the executor
exec, err := remotecommand.NewSPDYExecutor(c.config, "POST", req.URL())
return remotecommand.NewSPDYExecutor(c.config, "POST", req.URL())
}

// Exec joins the given streams to the command or, if command is empty, to a
// shell running in the given pod.
func (c *Client) Exec(ctx context.Context, deployment, namespace string,
command []string, stdio io.ReadWriter, stderr io.Writer, tty bool) error {
exec, err := c.getExecutor(ctx, deployment, namespace, command, stdio,
stderr, tty)
if err != nil {
return err
return fmt.Errorf("couldn't get executor: %v", err)
}
// execute the command
return exec.Stream(remotecommand.StreamOptions{
Expand Down
58 changes: 58 additions & 0 deletions internal/k8s/spin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package k8s

import (
"context"
"fmt"
"io"
"sync"
"time"
)

const (
framerate = 50 * time.Millisecond
)

var (
charset = []string{`|`, `/`, `-`, `\`}
)

// spinAfter will wait for the given time period and if the given context is
// not cancelled will start animating a spinner on w until the given context
// is cancelled.
//
// If the given context is cancelled before the wait duration, nothing is
// written to w.
//
// The returned *sync.WaitGroup should be waited on to ensure the spinner
// finishes cleaning up the animation.
func spinAfter(ctx context.Context, w io.Writer, wait time.Duration) *sync.WaitGroup {
var wg sync.WaitGroup
wt := time.NewTimer(wait)
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-ctx.Done():
case <-wt.C:
spin(ctx, w)
}
}()
return &wg
}

// spin animates a spinner on w until ctx is cancelled.
func spin(ctx context.Context, w io.Writer) {
for {
select {
case <-ctx.Done():
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
fmt.Fprint(w, "\033[2K")
return
default:
for _, char := range charset {
fmt.Fprintf(w, "%s getting you a shell\r", char)
time.Sleep(framerate)
}
}
}
}

0 comments on commit 5bfb5f6

Please sign in to comment.