Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

polling helpers for stack plan/configuration state #953

Merged
merged 3 commits into from
Aug 14, 2024

Conversation

brandonc
Copy link
Collaborator

@brandonc brandonc commented Aug 6, 2024

Description

Stack configurations, stack plans, and stack plan operations are driven through their lifecycle through various states, and these can be repetitive and difficult to correctly handle when developing client applications. It would be really nice if the SDK provided some simple polling helpers to assist with building client applications.

Testing plan

Utilized this in canary tests. DM for details. Here's an example:

Waiting for configuration prepared:

waitForPrepared := client.StackConfigurations.AwaitPrepared(ctx, stack.LatestStackConfiguration.ID)
for {
    result := <-waitForPrepared
    if result.Error != nil {
        return fmt.Errorf("error waiting for status: %w", result.Error)
    }
    if result.Quit {
        logf("%s reached quit status %s", result.ID, result.Status)
        return nil
    }
    logf("%s had status %s", result.ID, result.Status)
}

Fan-in plans triggered by prepared configuration:

plans, err := client.StackPlans.ListByConfiguration(ctx, stack.LatestStackConfiguration.ID, nil)
if err != nil {
    return fmt.Errorf("error listing stack plans: %w", err)
}

c.logger.Debug().Msgf("Found %d plans for stack %s", len(plans.Items), stack.ID)

// Wait on all the plans to finish
planWaits := make([]<-chan tfe.WaitForStatusResult, len(plans.Items))
for idx, plan := range plans.Items {
    planWaits[idx] = client.StackPlans.AwaitTerminalState(ctx, plan.ID)
    logf("Waiting for plan %s to finish...", plan.ID)
}

// Fan-in all waitings
planFinished := make(chan tfe.WaitForStatusResult)
for _, ch := range planWaits {
go func() {
    for result := range ch {
        planFinished <- result
    }
}()
}

done := 0

loop:
for {
    select {
    case <-ctx.Done():
        return fmt.Errorf("context was canceled: %w", ctx.Err())
    case progress := <-planFinished:
        if progress.Error != nil {
            return fmt.Errorf("plan %q could not finish: %w", progress.ID, progress.Error)
        }
	logf("Plan %s has status %s", progress.ID, progress.Status)
	if progress.Quit {
            done += 1
            logf("Plan %s reached quit status %s", progress.ID, progress.Status)
            if done == len(plans.Items) {
                break loop
            }
        } else if progress.Status == "paused_planned" {
            err = client.StackPlans.Approve(ctx, progress.ID)
            if err != nil {
                return fmt.Errorf("error approving plan %q: %w", progress.ID, err)
            }
        }
    }
}

Copy link
Contributor

@DanielMSchmidt DanielMSchmidt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, there are two functions I would need to have added for the CLI if possible :)

@@ -20,6 +20,16 @@ type StackConfigurations interface {

// JSONSchemas returns a byte slice of the JSON schema for the stack configuration.
JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add an AwaitQueued? This would be helpful since only then the eventstream url is set :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added AwaitStatus that accepts a specific status, and renamed "AwaitPrepared" to "AwaitCompleted"

stack_plan.go Show resolved Hide resolved
// The channel will be closed when the stack configuration reaches a status indicating that or an error occurs. The
// read will be retried dependending on the configuration of the client. When the channel is closed,
// the last value will either be a terminal status or an error.
func (s stackConfigurations) AwaitPrepared(ctx context.Context, stackConfigurationID string) <-chan WaitForStatusResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what the advantage is to returning the read only channel as opposed to returning either the terminal status or error? The caller will inevitably have to write a range loop to read from it or am I missing some extra context here

Copy link
Contributor

@sebasslash sebasslash Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait I see the examples, it allows for easier embedding within a select.

DanielMSchmidt
DanielMSchmidt previously approved these changes Aug 14, 2024
@brandonc brandonc merged commit b59fea2 into main Aug 14, 2024
7 checks passed
@brandonc brandonc deleted the brandonc/stack_status_polling branch August 14, 2024 20:33
Copy link

Reminder to the contributor that merged this PR: if your changes have added important functionality or fixed a relevant bug, open a follow-up PR to update CHANGELOG.md with a note on your changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants