Skip to content

Instantly share code, notes, and snippets.

@EvanBoyle
Created July 1, 2020 21:27
Show Gist options
  • Save EvanBoyle/a9fd29f1531dd8a63316f05afda34a02 to your computer and use it in GitHub Desktop.
Save EvanBoyle/a9fd29f1531dd8a63316f05afda34a02 to your computer and use it in GitHub Desktop.
package main
import (
"fmt"
"os"
"time"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
"github.com/pulumi/pulumi/x/automation"
)
func main() {
// login is global per CLI binary
err := login()
if err != nil {
os.Exit(1)
}
// formalization of the stack schema
sd := &automation.StackDescriptor{
Org: "acme",
Project: "api",
Stack: "prod",
}
stack, err := getOrCreateStack(sd)
maxRetries := 5
for current := 1; current <= maxRetries; current++ {
upRes, err := runPulumiUp(stack)
if err != nil {
switch err.Type {
case automation.UpError.Conflict:
// backoff and try again
time.Sleep(1 * time.Minute)
break
case automation.UpError.PendingOperations:
_, err := repairStack(stack)
if err != nil {
// unable to resolve pending operations
// notify a human
os.Exit(1)
}
// immediately retry
break
}
} else {
// successful update
upSummary, err := monitorUpdateToCompletion(stack, upRes.UpdateID)
if err != nil {
os.Exit(1)
}
fmt.Printf("updated %d resources, %d unchanged.", len(upSummary.Updates), len(upSummary.NoOps))
os.Exit(0)
}
}
// unable to execute successful update in 5 retries
os.Exit(1)
}
func login() error {
// login only supported via auth token
pulumiAccessToken := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
_, err := automation.Login(pulumiAccessToken)
return err
}
func getOrCreateStack(sd automation.StackDescriptor) (automation.Stack, error) {
stack, err := automation.SelectStack(sd)
if err != nil {
stack, err = automation.InitStack(sd)
if err != nil {
return nil, err
}
}
return stack, nil
}
func runPulumiUp(stack automation.Stack) (automation.UpResult, error) {
// the actual `pulumi` code.
upFn := func(ctx *pulumi.Context) error {
ctx.Export("x", pulumi.String("foo"))
return nil
}
upOpts := &automation.UpOptions{
Message: "automated update",
Parallel: 8,
}
// kick off an async update `pulumi up --async`
// this indicates that the update started successfully
// will run to completion in the background
// returns a result object with an UpdateID that can
// be used to poll for update status
upRes, err := automation.Up(stack, upFn, upOpts)
return upRes, err
}
func repairStack(stack automation.Stack) (automation.RefreshResult, error) {
// TODO: locking https://github.com/pulumi/pulumi/issues/4939
var stackBytes []byte
_, err := automation.StackExport(stack, stackBytes)
if err != nil {
return nil, err
}
_, err = automation.StackImport(stack, stackBytes)
if err != nil {
return nil, err
}
refreshRes, err := automation.Refresh(stack)
return refreshRes, err
}
func monitorUpdateToCompletion(stack automation.Stack, updateId string) (automation.UpdateSummary, error) {
var updateComplete bool
var updateStatus automation.UpdateStatus
for !updateComplete {
// a new pulumi command `pulumi stack status --updateID="xxxxxx"`
// updateID defaults to latest, allows for more of an "async" update style.
var err error
updateStatus, err = automation.StackUpdateStatus(stack, updateId)
if err != nil {
return nil, err
}
if automation.IsTerminalStatus(updateStatus.Status) {
updateComplete = true
}
time.Sleep(30 * time.Second)
}
return updateStatus, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment