[cloud] run tasks integration
This change will await the completion of pre-apply run tasks if they exist on a run and then report the results. It also adds an abstraction when interacting with cloud integrations such as policy checking and cost estimation that simplify and unify output, although I did not go so far as to refactor those callers to use it yet.
This commit is contained in:
parent
0900c7e0bb
commit
3e9ae69a12
1
go.mod
1
go.mod
|
@ -42,6 +42,7 @@ require (
|
||||||
github.com/hashicorp/go-plugin v1.4.3
|
github.com/hashicorp/go-plugin v1.4.3
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0
|
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||||
github.com/hashicorp/go-tfe v0.21.0
|
github.com/hashicorp/go-tfe v0.21.0
|
||||||
|
github.com/hashicorp/go-safetemp v1.0.0
|
||||||
github.com/hashicorp/go-uuid v1.0.2
|
github.com/hashicorp/go-uuid v1.0.2
|
||||||
github.com/hashicorp/go-version v1.3.0
|
github.com/hashicorp/go-version v1.3.0
|
||||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -407,8 +407,8 @@ github.com/hashicorp/go-slug v0.7.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
github.com/hashicorp/go-tfe v0.21.0 h1:P1QoeLkigDi4BXGQ//42kyXwfcHUqbh5jJemml6iQJs=
|
github.com/hashicorp/go-tfe v0.20.1-0.20211210153426-243d1b1eb033 h1:SoQhT2l6xHh7LOprw1F6MPzxSk65kSXdyfubOs8D+Ho=
|
||||||
github.com/hashicorp/go-tfe v0.21.0/go.mod h1:gyXLXbpBVxA2F/6opah8XBsOkZJxHYQmghl0OWi8keI=
|
github.com/hashicorp/go-tfe v0.20.1-0.20211210153426-243d1b1eb033/go.mod h1:gyXLXbpBVxA2F/6opah8XBsOkZJxHYQmghl0OWi8keI=
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||||
|
|
|
@ -299,7 +299,7 @@ func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Op
|
||||||
deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1)
|
deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1)
|
||||||
|
|
||||||
if b.CLI != nil {
|
if b.CLI != nil {
|
||||||
b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
|
b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n"))
|
||||||
b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount)))
|
b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount)))
|
||||||
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr)))
|
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr)))
|
||||||
|
|
||||||
|
|
|
@ -351,9 +351,24 @@ in order to capture the filesystem context the remote workspace expects:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await collective run tasks
|
||||||
|
if len(r.TaskStage) > 0 {
|
||||||
|
context := NewIntegrationContext(stopCtx, cancelCtx, b, op, r)
|
||||||
|
err = b.runTasks(context, context.BeginOutput("Run Tasks (pre-apply)"), r.TaskStage[0].ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a pointer to the given string.
|
||||||
|
func String(v string) *string {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
const planDefaultHeader = `
|
const planDefaultHeader = `
|
||||||
[reset][yellow]Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
|
[reset][yellow]Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
|
||||||
will stop streaming the logs, but will not stop the plan running remotely.[reset]
|
will stop streaming the logs, but will not stop the plan running remotely.[reset]
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
package cloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-tfe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type taskResultSummary struct {
|
||||||
|
pending int
|
||||||
|
failed int
|
||||||
|
failedMandatory int
|
||||||
|
passed int
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskStageReadFunc func(b *Cloud, stopCtx context.Context) (*tfe.TaskStage, error)
|
||||||
|
|
||||||
|
func summarizeTaskResults(taskResults []*tfe.TaskResult) taskResultSummary {
|
||||||
|
var pe, er, erm, pa int
|
||||||
|
for _, task := range taskResults {
|
||||||
|
if task.Status == "running" || task.Status == "pending" {
|
||||||
|
pe++
|
||||||
|
} else if task.Status == "passed" {
|
||||||
|
pa++
|
||||||
|
} else {
|
||||||
|
// Everything else is a failure
|
||||||
|
er++
|
||||||
|
if task.WorkspaceTaskEnforcementLevel == "mandatory" {
|
||||||
|
erm++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskResultSummary{
|
||||||
|
pending: pe,
|
||||||
|
failed: er,
|
||||||
|
failedMandatory: erm,
|
||||||
|
passed: pa,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// elapsedMessageMax is 50 chars: the length of this message with 6 digits
|
||||||
|
// 99 tasks still pending, 99 passed, 99 failed ...
|
||||||
|
const elapsedMessageMax int = 50
|
||||||
|
|
||||||
|
func (b *Cloud) runTasksWithTaskResults(context *IntegrationContext, output IntegrationOutputWriter, fetchTaskStage taskStageReadFunc) error {
|
||||||
|
// TODO: Do not fetch run tasks if there are no changes to infrastructure
|
||||||
|
// if !context.Run.HasChanges {
|
||||||
|
// output.Output("No Changes. Tasks will not run")
|
||||||
|
// output.End()
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
return context.Poll(func(i int) (bool, error) {
|
||||||
|
// TODO: get the stage that corresponds to an argument passed to this function
|
||||||
|
stage, err := fetchTaskStage(b, context.StopContext)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, generalError("Failed to retrieve pre-apply task stage", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := summarizeTaskResults(stage.TaskResults)
|
||||||
|
if summary.pending > 0 {
|
||||||
|
message := fmt.Sprintf("%d tasks still pending, %d passed, %d failed ... ", summary.pending, summary.passed, summary.failed)
|
||||||
|
|
||||||
|
if i%4 == 0 {
|
||||||
|
if i > 0 {
|
||||||
|
output.OutputElapsed(message, elapsedMessageMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more tasks pending/running. Print all the results.
|
||||||
|
|
||||||
|
// Track the first task name that is a mandatory enforcement level breach.
|
||||||
|
var firstMandatoryTaskFailed *string = nil
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
output.Output(fmt.Sprintf("All tasks completed! %d passed, %d failed", summary.passed, summary.failed))
|
||||||
|
} else {
|
||||||
|
output.OutputElapsed(fmt.Sprintf("All tasks completed! %d passed, %d failed", summary.passed, summary.failed), 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Output("")
|
||||||
|
|
||||||
|
for _, t := range stage.TaskResults {
|
||||||
|
capitalizedStatus := string(t.Status)
|
||||||
|
capitalizedStatus = strings.ToUpper(capitalizedStatus[:1]) + capitalizedStatus[1:]
|
||||||
|
|
||||||
|
status := "[green]" + capitalizedStatus
|
||||||
|
if t.Status != "passed" {
|
||||||
|
level := string(t.WorkspaceTaskEnforcementLevel)
|
||||||
|
level = strings.ToUpper(level[:1]) + level[1:]
|
||||||
|
status = fmt.Sprintf("[red]%s (%s)", capitalizedStatus, level)
|
||||||
|
|
||||||
|
if t.WorkspaceTaskEnforcementLevel == "mandatory" && firstMandatoryTaskFailed == nil {
|
||||||
|
firstMandatoryTaskFailed = &t.TaskName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf(`%s ⸺ %s`, t.TaskName, status)
|
||||||
|
output.SubOutput(title)
|
||||||
|
|
||||||
|
output.SubOutput(fmt.Sprintf("[dim]%s", t.Message))
|
||||||
|
output.SubOutput("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a mandatory enforcement level is breached, return an error.
|
||||||
|
var taskErr error = nil
|
||||||
|
var overall string = "[green]Passed"
|
||||||
|
if firstMandatoryTaskFailed != nil {
|
||||||
|
overall = "[red]Failed"
|
||||||
|
taskErr = fmt.Errorf("the run failed because the run task, %s, is required to succeed", *firstMandatoryTaskFailed)
|
||||||
|
} else if summary.failed > 0 { // we have failures but none of them mandatory
|
||||||
|
overall = "[green]Passed with advisory failures"
|
||||||
|
}
|
||||||
|
|
||||||
|
output.SubOutput("")
|
||||||
|
output.SubOutput("[bold]Overall Result: " + overall)
|
||||||
|
|
||||||
|
output.End()
|
||||||
|
|
||||||
|
return false, taskErr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Cloud) runTasks(ctx *IntegrationContext, output IntegrationOutputWriter, stageID string) error {
|
||||||
|
return b.runTasksWithTaskResults(ctx, output, func(b *Cloud, stopCtx context.Context) (*tfe.TaskStage, error) {
|
||||||
|
options := tfe.TaskStageReadOptions{
|
||||||
|
Include: "task_results",
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.client.TaskStages.Read(ctx.StopContext, stageID, &options)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package cloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-tfe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testIntegrationOutput struct {
|
||||||
|
ctx *IntegrationContext
|
||||||
|
output *strings.Builder
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ IntegrationOutputWriter = (*testIntegrationOutput)(nil) // Compile time check
|
||||||
|
|
||||||
|
func (s *testIntegrationOutput) End() {
|
||||||
|
s.output.WriteString("END\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testIntegrationOutput) SubOutput(str string) {
|
||||||
|
s.output.WriteString(s.ctx.B.Colorize().Color("[reset]│ "+str) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testIntegrationOutput) Output(str string) {
|
||||||
|
s.output.WriteString(s.ctx.B.Colorize().Color("[reset]│ ") + str + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testIntegrationOutput) OutputElapsed(message string, maxMessage int) {
|
||||||
|
s.output.WriteString("PENDING MESSAGE: " + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockIntegrationContext(b *Cloud, t *testing.T) (*IntegrationContext, *testIntegrationOutput) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Retrieve the workspace used to run this operation in.
|
||||||
|
w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error retrieving workspace: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new configuration version.
|
||||||
|
c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating configuration version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a pending run to block this run.
|
||||||
|
r, err := b.client.Runs.Create(ctx, tfe.RunCreateOptions{
|
||||||
|
ConfigurationVersion: c,
|
||||||
|
Workspace: w,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating pending run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
|
||||||
|
defer configCleanup()
|
||||||
|
defer done(t)
|
||||||
|
|
||||||
|
integrationContext := &IntegrationContext{
|
||||||
|
B: b,
|
||||||
|
StopContext: ctx,
|
||||||
|
CancelContext: ctx,
|
||||||
|
Op: op,
|
||||||
|
Run: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationContext, &testIntegrationOutput{
|
||||||
|
ctx: integrationContext,
|
||||||
|
output: &strings.Builder{},
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloud_runTasksWithTaskResults(t *testing.T) {
|
||||||
|
b, bCleanup := testBackendWithName(t)
|
||||||
|
defer bCleanup()
|
||||||
|
|
||||||
|
integrationContext, writer := newMockIntegrationContext(b, t)
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
taskResults []*tfe.TaskResult
|
||||||
|
context *IntegrationContext
|
||||||
|
writer *testIntegrationOutput
|
||||||
|
expectedOutputs []string
|
||||||
|
isError bool
|
||||||
|
}{
|
||||||
|
"all-succeeded": {
|
||||||
|
taskResults: []*tfe.TaskResult{
|
||||||
|
{ID: "1", TaskName: "Mandatory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "mandatory"},
|
||||||
|
{ID: "2", TaskName: "Advisory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "advisory"},
|
||||||
|
},
|
||||||
|
writer: writer,
|
||||||
|
context: integrationContext,
|
||||||
|
expectedOutputs: []string{"Overall Result: Passed\n"},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"mandatory-failed": {
|
||||||
|
taskResults: []*tfe.TaskResult{
|
||||||
|
{ID: "1", TaskName: "Mandatory", Message: "500 Error", Status: "failed", WorkspaceTaskEnforcementLevel: "mandatory"},
|
||||||
|
{ID: "2", TaskName: "Advisory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "advisory"},
|
||||||
|
},
|
||||||
|
writer: writer,
|
||||||
|
context: integrationContext,
|
||||||
|
expectedOutputs: []string{"Passed\n", "A-OK\n", "Overall Result: Failed\n"},
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
"advisory-failed": {
|
||||||
|
taskResults: []*tfe.TaskResult{
|
||||||
|
{ID: "1", TaskName: "Mandatory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "mandatory"},
|
||||||
|
{ID: "2", TaskName: "Advisory", Message: "500 Error", Status: "failed", WorkspaceTaskEnforcementLevel: "advisory"},
|
||||||
|
},
|
||||||
|
writer: writer,
|
||||||
|
context: integrationContext,
|
||||||
|
expectedOutputs: []string{"Failed (Advisory)", "Overall Result: Passed with advisory failure"},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for caseName, c := range cases {
|
||||||
|
c.writer.output.Reset()
|
||||||
|
err := b.runTasksWithTaskResults(c.writer.ctx, c.writer, func(b *Cloud, stopCtx context.Context) (*tfe.TaskStage, error) {
|
||||||
|
return &tfe.TaskStage{
|
||||||
|
TaskResults: c.taskResults,
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if c.isError && err == nil {
|
||||||
|
t.Fatalf("Expected %s to be error", caseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.isError && err != nil {
|
||||||
|
t.Errorf("Expected %s to not be error but received %s", caseName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := c.writer.output.String()
|
||||||
|
for _, expected := range c.expectedOutputs {
|
||||||
|
if !strings.Contains(output, expected) {
|
||||||
|
t.Fatalf("Expected output to contain '%s' but it was:\n\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package cloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/internal/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntegrationOutputWriter interface {
|
||||||
|
End()
|
||||||
|
OutputElapsed(message string, maxMessage int)
|
||||||
|
Output(str string)
|
||||||
|
SubOutput(str string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntegrationContext struct {
|
||||||
|
started time.Time
|
||||||
|
B *Cloud
|
||||||
|
StopContext context.Context
|
||||||
|
CancelContext context.Context
|
||||||
|
Op *backend.Operation
|
||||||
|
Run *tfe.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
type integrationCLIOutput struct {
|
||||||
|
ctx *IntegrationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ IntegrationOutputWriter = (*integrationCLIOutput)(nil) // Compile time check
|
||||||
|
|
||||||
|
func NewIntegrationContext(stopCtx, cancelCtx context.Context, b *Cloud, op *backend.Operation, r *tfe.Run) *IntegrationContext {
|
||||||
|
return &IntegrationContext{
|
||||||
|
B: b,
|
||||||
|
StopContext: stopCtx,
|
||||||
|
CancelContext: cancelCtx,
|
||||||
|
Op: op,
|
||||||
|
Run: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationContext) Poll(every func(i int) (bool, error)) error {
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
select {
|
||||||
|
case <-s.StopContext.Done():
|
||||||
|
return s.StopContext.Err()
|
||||||
|
case <-s.CancelContext.Done():
|
||||||
|
return s.CancelContext.Err()
|
||||||
|
case <-time.After(backoff(backoffMin, backoffMax, i)):
|
||||||
|
// blocks for a time between min and max
|
||||||
|
}
|
||||||
|
|
||||||
|
cont, err := every(i)
|
||||||
|
if !cont {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationContext) BeginOutput(name string) IntegrationOutputWriter {
|
||||||
|
var result IntegrationOutputWriter = &integrationCLIOutput{
|
||||||
|
ctx: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.started = time.Now()
|
||||||
|
|
||||||
|
if s.HasCLI() {
|
||||||
|
s.B.CLI.Output("\n------------------------------------------------------------------------\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Output("[bold]" + name + ":\n")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationContext) HasCLI() bool {
|
||||||
|
return s.B.CLI != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *integrationCLIOutput) End() {
|
||||||
|
if !s.ctx.HasCLI() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ctx.B.CLI.Output("\n------------------------------------------------------------------------\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *integrationCLIOutput) Output(str string) {
|
||||||
|
if !s.ctx.HasCLI() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ctx.B.CLI.Output(s.ctx.B.Colorize().Color(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *integrationCLIOutput) SubOutput(str string) {
|
||||||
|
if !s.ctx.HasCLI() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ctx.B.CLI.Output(s.ctx.B.Colorize().Color(fmt.Sprintf("[reset]│ %s", str)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example pending output; the variable spacing (50 chars) allows up to 99 tasks (two digits) in each category:
|
||||||
|
// ---------------
|
||||||
|
// 13 tasks still pending, 0 passed, 0 failed ...
|
||||||
|
// 13 tasks still pending, 0 passed, 0 failed ... (8s elapsed)
|
||||||
|
// 13 tasks still pending, 0 passed, 0 failed ... (19s elapsed)
|
||||||
|
// 13 tasks still pending, 0 passed, 0 failed ... (33s elapsed)
|
||||||
|
func (s *integrationCLIOutput) OutputElapsed(message string, maxMessage int) {
|
||||||
|
if !s.ctx.HasCLI() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elapsed := time.Since(s.ctx.started).Truncate(1 * time.Second)
|
||||||
|
s.ctx.B.CLI.Output(fmt.Sprintf("%-"+strconv.FormatInt(int64(maxMessage), 10)+"s", message) + s.ctx.B.Colorize().Color(fmt.Sprintf("[dim](%s elapsed)", elapsed)))
|
||||||
|
}
|
Loading…
Reference in New Issue