Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **`spawn autoscale` honours `--region` instead of silently ignoring it**
(#774). The root `--region` flag — whose help says it "overrides
SPORE_REGION/AWS_REGION and the shared config" — overrode nothing: every
autoscale call site built its AWS config with a bare
`config.LoadDefaultConfig(ctx)`, taking the region from the ambient chain. It
was ignored in *both* directions, with `AWS_REGION` winning even when
`--region` contradicted it.
The symptoms: the groups listing failed with `ResourceNotFoundException` in any
region without a groups table; `autoscale health` reported a healthy group as
having no instances; and **`autoscale terminate` would find nothing to
terminate and report success while the instances kept billing**. It also made
#772's new coverage check report "no autoscale orchestrator runs in this
account" against an account that has one.
All **four** call sites now route through one resolver, which fixes the shared
*profile* they also ignored and uses the same precedence as every other command
(explicit region > shared config > ambient). Two of the four were found by the
new gate, not by me — `autoscale health` and `autoscale terminate` both had
`cfg, _ := config.LoadDefaultConfig(ctx)`, discarding the error as well.

- **Three `pipeline-orchestrator` Makefile targets named a Lambda that does not
exist** (#754). `make update-code`, `make logs` and `make invoke-test` all
asked for `spawn-pipeline-orchestrator-production`, but `template.yaml` sets
Expand Down
8 changes: 6 additions & 2 deletions cmd/autoscale_coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"fmt"
"io"

"github.com/aws/aws-sdk-go-v2/config"
spawnaws "github.com/spore-host/spawn/pkg/aws"
)

Expand All @@ -26,7 +25,12 @@
// turn a successful launch into a failure, so every error path here degrades to
// either a hedged note or silence.
func reportAutoscaleCoverage(ctx context.Context, w io.Writer) spawnaws.AutoscaleCoverage {
cfg, err := config.LoadDefaultConfig(ctx)
// The SAME resolver the rest of the command uses (spawn#774), so coverage
// cannot report on a different region than the one being operated on. It
// previously called config.LoadDefaultConfig directly and reported "no
// autoscale orchestrator runs in this account" against an account that has
// one, because the ambient region was us-west-2.
cfg, err := autoscaleConfig(ctx)

Check warning on line 33 in cmd/autoscale_coverage.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_coverage.go#L33

Added line #L33 was not covered by tests
if err != nil {
// Nothing useful to say, and the caller's real work already succeeded.
return spawnaws.AutoscaleCoverage{Why: err.Error()}
Expand Down
34 changes: 30 additions & 4 deletions cmd/autoscale_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,46 @@
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/lambda"
lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/spore-host/spawn/pkg/autoscaler"
spawnaws "github.com/spore-host/spawn/pkg/aws"
)

// autoscaleConfig resolves the AWS config for every autoscale call site.
//
// This is spawn#774. Both sites used to call config.LoadDefaultConfig(ctx) with
// no options, which took the region from the ambient chain and IGNORED the
// root --region flag — a flag whose own help text says it "overrides
// SPORE_REGION/AWS_REGION and the shared config". It overrode nothing:
//
// $ spawn autoscale status --region us-east-1 # probed us-west-2
// $ AWS_REGION=us-east-1 … --region us-west-2 # used us-east-1
//
// Ignored in both directions, with the env winning over the flag that claims to
// beat it. The visible symptoms were a DynamoDB scan failing with
// ResourceNotFoundException in a region holding no groups table, and
// triggerLambda invoking a function by name in a region where it may not exist.
//
// Routing through NewClientWithRegion fixes the shared PROFILE too, which these
// sites also ignored, and uses the same resolution order as every other command
// (explicit region > sporeconfig shared region > ambient).
func autoscaleConfig(ctx context.Context) (aws.Config, error) {
client, err := spawnaws.NewClientWithRegion(ctx, spawnRegion)
if err != nil {
return aws.Config{}, fmt.Errorf("load AWS config: %w", err)

Check warning on line 39 in cmd/autoscale_helpers.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_helpers.go#L36-L39

Added lines #L36 - L39 were not covered by tests
}
return client.Config(), nil

Check warning on line 41 in cmd/autoscale_helpers.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_helpers.go#L41

Added line #L41 was not covered by tests
}

func getAutoscaler(ctx context.Context) (*autoscaler.AutoScaler, error) {
cfg, err := config.LoadDefaultConfig(ctx)
cfg, err := autoscaleConfig(ctx)

Check warning on line 45 in cmd/autoscale_helpers.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_helpers.go#L45

Added line #L45 was not covered by tests
if err != nil {
return nil, fmt.Errorf("load AWS config: %w", err)
return nil, err

Check warning on line 47 in cmd/autoscale_helpers.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_helpers.go#L47

Added line #L47 was not covered by tests
}

// Build table name with environment suffix
Expand All @@ -40,7 +66,7 @@
}

func triggerLambda(ctx context.Context, groupID string) error {
cfg, err := config.LoadDefaultConfig(ctx)
cfg, err := autoscaleConfig(ctx)

Check warning on line 69 in cmd/autoscale_helpers.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_helpers.go#L69

Added line #L69 was not covered by tests
if err != nil {
return err
}
Expand Down
13 changes: 10 additions & 3 deletions cmd/autoscale_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"log"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -140,8 +139,16 @@
return fmt.Errorf("get group: %w", err)
}

// Terminate all instances
cfg, _ := config.LoadDefaultConfig(ctx)
// Terminate all instances.
//
// The region matters here more than anywhere else in this command (#774):
// with the wrong one this finds NOTHING to terminate and reports success
// while the instances keep billing. The error is handled rather than
// discarded for the same reason.
cfg, err := autoscaleConfig(ctx)
if err != nil {
return err

Check warning on line 150 in cmd/autoscale_lifecycle.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_lifecycle.go#L148-L150

Added lines #L148 - L150 were not covered by tests
}
ec2Client := ec2.NewFromConfig(cfg)

result, err := ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
Expand Down
68 changes: 68 additions & 0 deletions cmd/autoscale_region_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cmd

import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

// TestAutoscaleHonoursTheRegionFlag guards spawn#774.
//
// `spawn autoscale` advertised the root --region flag — whose help says it
// "overrides SPORE_REGION/AWS_REGION and the shared config" — and ignored it.
// Both call sites built their AWS config with a bare
// config.LoadDefaultConfig(ctx), so the region came from the ambient chain and
// the flag did nothing. The flag was ignored in BOTH directions: AWS_REGION won
// even when --region contradicted it.
//
// Symptoms were a DynamoDB scan failing with ResourceNotFoundException against a
// region with no groups table, triggerLambda invoking a function by name in a
// region where it may not exist, and the #772 coverage check confidently
// reporting "no autoscale orchestrator runs in this account" against an account
// that has one.
//
// A source assertion rather than a behavioural one, deliberately: proving a
// region reached the SDK needs either a live account or an injected client, and
// neither exists on this path today. What is cheap and sufficient is forbidding
// the construct that caused it — an unparameterised LoadDefaultConfig in the
// autoscale call sites, which must go through autoscaleConfig instead.
func TestAutoscaleHonoursTheRegionFlag(t *testing.T) {
// Matches a LoadDefaultConfig call with NO options — the broken form.
// `config.LoadDefaultConfig(ctx, config.WithRegion(r))` is fine and does not
// match, which is what distinguishes this from a blanket ban.
bare := regexp.MustCompile(`config\.LoadDefaultConfig\(\s*ctx\s*\)`)

files, err := filepath.Glob("autoscale*.go")
if err != nil {
t.Fatalf("glob: %v", err)
}
if len(files) == 0 {
t.Fatal("no autoscale*.go files found — this gate is matching nothing")
}

for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
b, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
// Strip comments so the explanation above (which names the broken call)
// does not satisfy the matcher. Three gates this session passed against
// their own prose; this one is written knowing that.
for _, line := range strings.Split(string(b), "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "//") {
continue
}
if bare.MatchString(line) {
t.Errorf("%s: config.LoadDefaultConfig(ctx) with no options ignores the --region "+
"flag and the shared profile (spawn#774). Use autoscaleConfig(ctx), which "+
"routes through aws.NewClientWithRegion(ctx, spawnRegion).\n %s",
f, strings.TrimSpace(line))
}
}
}
}
10 changes: 7 additions & 3 deletions cmd/autoscale_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
Expand Down Expand Up @@ -100,8 +99,13 @@
return fmt.Errorf("get group: %w", err)
}

// Get instances
cfg, _ := config.LoadDefaultConfig(ctx)
// Get instances. Region-resolved like every other autoscale call site
// (#774); a wrong region here reports a healthy group as having no
// instances.
cfg, err := autoscaleConfig(ctx)
if err != nil {
return err

Check warning on line 107 in cmd/autoscale_status.go

View check run for this annotation

Codecov / codecov/patch

cmd/autoscale_status.go#L105-L107

Added lines #L105 - L107 were not covered by tests
}
ec2Client := ec2.NewFromConfig(cfg)

result, err := ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
Expand Down
Loading