Skip to content

Commit

Permalink
Runs and task and logs, oh my
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinwyszynski committed Apr 7, 2021
1 parent 5f09f87 commit 59d20f4
Show file tree
Hide file tree
Showing 14 changed files with 481 additions and 8 deletions.
28 changes: 24 additions & 4 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package client

import (
"context"
"fmt"
"net/http"
"net/url"

"github.com/shurcooL/graphql"
"golang.org/x/oauth2"
Expand All @@ -15,22 +17,40 @@ type client struct {
session session.Session
}

func (c *client) Query(ctx context.Context, query interface{}, variables map[string]interface{}) error {
// New returns a new instance of a Spacelift Client.
func New(wraps *http.Client, session session.Session) Client {
return &client{wraps: wraps, session: session}
}

func (c *client) Mutate(ctx context.Context, mutation interface{}, variables map[string]interface{}) error {
apiClient, err := c.apiClient(ctx)
if err != nil {
return nil
}

return apiClient.Query(ctx, query, variables)
return apiClient.Mutate(ctx, mutation, variables)
}

func (c *client) Mutate(ctx context.Context, mutation interface{}, variables map[string]interface{}) error {
func (c *client) Query(ctx context.Context, query interface{}, variables map[string]interface{}) error {
apiClient, err := c.apiClient(ctx)
if err != nil {
return nil
}

return apiClient.Mutate(ctx, mutation, variables)
return apiClient.Query(ctx, query, variables)
}

func (c *client) URL(format string, a ...interface{}) string {
endpoint := c.session.Endpoint()

endpointURL, err := url.Parse(endpoint)
if err != nil {
panic(err) // Impossible condition.
}

endpointURL.Path = fmt.Sprintf(format, a...)

return endpointURL.String()
}

func (c *client) apiClient(ctx context.Context) (*graphql.Client, error) {
Expand Down
5 changes: 4 additions & 1 deletion client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ type Client interface {
Query(context.Context, interface{}, map[string]interface{}) error

// Mutate executes a single GraphQL mutation request.
Mutate(context.Context, interface{}, map[string]interface{})
Mutate(context.Context, interface{}, map[string]interface{}) error

// URL returns a full URL given a formatted path.
URL(string, ...interface{}) string
}
4 changes: 4 additions & 0 deletions client/structs/run_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package structs

// RunState is the state of the run.
type RunState string
42 changes: 42 additions & 0 deletions client/structs/run_state_transition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package structs

import (
"fmt"
"strings"
"time"
)

// RunStateTransition represents a single run state transition.
type RunStateTransition struct {
HasLogs bool `graphql:"hasLogs"`
Note *string `graphql:"note"`
State RunState `graphql:"state"`
Terminal bool `graphql:"terminal"`
Timestamp int `graphql:"timestamp"`
Username *string `graphql:"username"`
}

func (r *RunStateTransition) About() string {
parts := []string{
string(r.State),
time.Unix(int64(r.Timestamp), 0).Format(time.UnixDate),
}

if username := r.Username; username != nil {
parts = append(parts, *username)
}

if note := r.Note; note != nil {
parts = append(parts, *note)
}

return strings.Join(parts, "\t")
}

func (r *RunStateTransition) Error() error {
if r.State == RunState("FINISHED") {
return nil
}

return fmt.Errorf("finished with %s state", r.State)
}
9 changes: 9 additions & 0 deletions client/structs/run_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package structs

// RunType is the type of the run.
type RunType string

func NewRunType(in string) *RunType {
out := RunType(in)
return &out
}
16 changes: 16 additions & 0 deletions cmd/internal/actions/multi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package actions

import "github.com/urfave/cli/v2"

// Multi combines multiple CLI actions.
func Multi(steps ...cli.BeforeFunc) cli.BeforeFunc {
return func(ctx *cli.Context) error {
for _, step := range steps {
if err := step(ctx); err != nil {
return err
}
}

return nil
}
}
26 changes: 26 additions & 0 deletions cmd/internal/authenticated/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package authenticated

import (
"github.com/urfave/cli/v2"

"github.com/spacelift-io/spacelift-cli/client"
"github.com/spacelift-io/spacelift-cli/client/session"
)

// Client is the authenticated client that can be used by all CLI commands.
var Client client.Client

// Ensure is a way of ensuring that the Client exists, and it meant to be used
// as a Before action for commands that need it.
func Ensure(*cli.Context) error {
ctx, httpClient := session.Defaults()

session, err := session.New(ctx, httpClient)
if err != nil {
return err
}

Client = client.New(httpClient, session)

return nil
}
12 changes: 12 additions & 0 deletions cmd/internal/stack/before_each.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package stack

import (
"github.com/urfave/cli/v2"
)

var stackID string

func beforeEach(cliCtx *cli.Context) error {
stackID = cliCtx.String(flagStackID.Name)
return nil
}
32 changes: 32 additions & 0 deletions cmd/internal/stack/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package stack

import "github.com/urfave/cli/v2"

var flagStackID = &cli.StringFlag{
Name: "id",
Usage: "User-facing ID (slug) of the stack",
Required: true,
}

var flagCommitSHA = &cli.StringFlag{
Name: "sha",
Usage: "Commit SHA for the newly created run",
}

var flagRun = &cli.StringFlag{
Name: "run",
Usage: "ID of the run",
Required: true,
}

var flagNoInit = &cli.BoolFlag{
Name: "noinit",
Usage: "Indicate whether to skip initialization for a task",
Value: false,
}

var flagTail = &cli.BoolFlag{
Name: "tail",
Usage: "Indicate whether to tail the run",
Value: false,
}
147 changes: 147 additions & 0 deletions cmd/internal/stack/run_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package stack

import (
"context"
"errors"
"fmt"
"time"

"github.com/shurcooL/graphql"

"github.com/spacelift-io/spacelift-cli/client/structs"
"github.com/spacelift-io/spacelift-cli/cmd/internal/authenticated"
)

func runLogs(ctx context.Context, stack, run string) (err error) {
lines := make(chan string)

go func() {
err = runStates(ctx, stack, run, lines)
}()

for line := range lines {
fmt.Print(line)
}

return err
}

func runStates(ctx context.Context, stack, run string, sink chan<- string) error {
defer func() { close(sink) }()

var query struct {
Stack *struct {
Run *struct {
History []structs.RunStateTransition `graphql:"history"`
} `graphql:"run(id: $run)"`
} `graphql:"stack(id: $stack)"`
}

variables := map[string]interface{}{
"stack": graphql.ID(stack),
"run": graphql.ID(run),
}

reportedStates := make(map[structs.RunState]struct{})

for {
if err := authenticated.Client.Query(ctx, &query, variables); err != nil {
return err
}

if query.Stack == nil || query.Stack.Run == nil {
return errors.New("not found")
}

history := query.Stack.Run.History

for index := range history {
// Unlike the GUI, we go earliest first.
transition := history[len(history)-index-1]

if _, ok := reportedStates[transition.State]; ok {
continue
}
reportedStates[transition.State] = struct{}{}

fmt.Println("")
fmt.Println("-----------------")
fmt.Println(transition.About())
fmt.Println("-----------------")
fmt.Println("")

if transition.HasLogs {
if err := runStateLogs(ctx, stack, run, transition.State, sink); err != nil {
return err
}
}

if transition.Terminal {
return transition.Error()
}
}

// TODO: Increase the timeout every time there's nothing new.
time.Sleep(5 * time.Second)
}
}

func runStateLogs(ctx context.Context, stack, run string, state structs.RunState, sink chan<- string) error {
var query struct {
Stack *struct {
Run *struct {
Logs *struct {
Exists bool `graphql:"exists"`
Finished bool `graphql:"finished"`
HasMore bool `graphql:"hasMore"`
Messages []struct {
Body string `graphql:"message"`
} `graphql:"messages"`
NextToken *graphql.String `graphql:"nextToken"`
} `graphql:"logs(state: $state, token: $token)"`
} `graphql:"run(id: $run)"`
} `graphql:"stack(id: $stack)"`
}

var token *graphql.String

variables := map[string]interface{}{
"stack": graphql.ID(stack),
"run": graphql.ID(run),
"state": state,
"token": token,
}

var backOff time.Duration

for {
if err := authenticated.Client.Query(ctx, &query, variables); err != nil {
return err
}

if query.Stack == nil || query.Stack.Run == nil || query.Stack.Run.Logs == nil {
return errors.New("not found")
}

logs := query.Stack.Run.Logs
variables["token"] = logs.NextToken

for _, message := range logs.Messages {
sink <- message.Body
}

if logs.Finished {
break
}

if logs.HasMore {
backOff = 0
} else {
backOff++
}

time.Sleep(backOff * time.Second)
}

return nil
}
Loading

0 comments on commit 59d20f4

Please sign in to comment.