Skip to content

feat(autoscaling_group): add infra.autoscaling_group driver#10

Merged
intel352 merged 7 commits into
mainfrom
feat/issue-9-autoscaling-group
May 13, 2026
Merged

feat(autoscaling_group): add infra.autoscaling_group driver#10
intel352 merged 7 commits into
mainfrom
feat/issue-9-autoscaling-group

Conversation

@intel352
Copy link
Copy Markdown
Contributor

Summary

  • Adds infra.autoscaling_group resource driver wrapping AWS Application Auto Scaling — covers ECS services, DynamoDB, RDS Aurora replicas, AppStream, and any Application Auto Scaling namespace.
  • Supports both TargetTrackingScaling and StepScaling policy types with separate configuration shapes for each.
  • Update is idempotent (RegisterScalableTarget) and reconciles policies: stale live policies absent from the desired spec are deleted before upserting the desired ones.
  • Promotes applicationautoscaling from indirect to direct dependency; plugs the driver into provider.registerDrivers and Capabilities, and adds "infra.autoscaling_group" to plugin.json.

Test plan

  • TDD: failing tests written first (go test ./drivers -run TestAutoScalingGroup -v confirmed build-fail before impl)
  • 24 unit tests passing: Create/Read/Update/Delete happy paths, missing-required-field errors, API errors, stale-policy removal, Diff, HealthCheck, SensitiveKeys, interface compliance
  • go build ./... clean
  • go test -race ./... clean — all packages pass

Notes

Blocker note: workflow#653 Phase 1 must land before this driver can be exercised end-to-end via the wfctl strict-contracts gate.

Closes #9

🤖 Generated with Claude Code

intel352 and others added 4 commits May 4, 2026 04:27
…CanonicalKeys} stubs — closes pre-existing gap from workflow#499

Adds the two missing IaCProvider methods that were introduced upstream after this
plugin's last workflow dep bump:

- `BootstrapStateBackend(ctx, cfg) (*BootstrapResult, error)` — returns (nil, nil)
  per the interface's documented contract for providers that do not manage a state
  backend. AWS state lives in S3 via a separate workflow path.
- `SupportedCanonicalKeys() []string` — returns the full `interfaces.CanonicalKeys()`
  set per the doc's "built-in and stub providers return the full canonical key set"
  guidance; per-driver field validation already happens in Diff.

Bumps `github.com/GoCodeAlone/workflow` from v0.3.56 to v0.19.2 because the
`interfaces.BootstrapResult` return type only exists in v0.18.6+. Without the dep
bump, the BootstrapStateBackend method signature would not compile against the
plugin's own CI.

Surfaced by workflow PR #534's new cross-plugin-build CI gate, which builds this
plugin against workflow main via a `replace` directive — exactly the gap class this
gate was designed to catch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… params

- Replaced multi-cloud "S3/GCS/Azure Blob" doc copy with AWS-specific reference.
- SupportedCanonicalKeys doc no longer claims per-driver Diff validation
  (drivers compare overlapping fields; do not reject unsupported keys).
- BootstrapStateBackend params switched to _ per repo's no-op convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps AWS Application Auto Scaling (RegisterScalableTarget,
DescribeScalableTargets, PutScalingPolicy, DescribeScalingPolicies,
DeleteScalingPolicy, DeregisterScalableTarget). Supports ECS, DynamoDB,
RDS, AppStream and any Application Auto Scaling namespace.

Key design points:
- ProviderID encodes "namespace|resource_id|dimension" for precise
  multi-key lookup across Read, Update, and Delete.
- Scaling policies: both TargetTrackingScaling and StepScaling policy
  types are supported with distinct config shapes per adversarial check.
- Update is idempotent (RegisterScalableTarget) and performs delete-on-
  removal for stale policies before upserting desired ones.
- 24 unit tests covering Create/Read/Update/Delete happy paths, missing
  required fields, API errors, stale-policy removal, Diff, HealthCheck,
  SensitiveKeys, and interface compliance.

Blocker note: workflow#653 Phase 1 must land before this driver can
be exercised via wfctl strict-contracts gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n schema)

Keep aws-sdk-go-v2 v1.41.7 (needed by autoscaling_group driver), adopt
main's richer SupportedCanonicalKeys (returns provider-specific keys),
and migrate plugin.json to the new capabilities-object schema with
autoscaling keyword added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new AWS resource driver to the plugin for managing AWS Application Auto Scaling scalable targets and scaling policies under the infra.autoscaling_group capability, and wires it into the provider and dependency graph.

Changes:

  • Register a new infra.autoscaling_group driver in AWSProvider and advertise it via Capabilities().
  • Introduce drivers/autoscaling_group.go implementing CRUD/Diff/HealthCheck for scalable targets and policy reconciliation.
  • Add unit tests for the new driver and update plugin metadata + Go module dependencies to include applicationautoscaling directly.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
provider/provider.go Registers the new driver and exposes the new capability.
provider/provider_test.go Updates capability-count and required-capability assertions.
plugin.json Updates plugin description/keywords to reflect autoscaling support.
go.mod Promotes service/applicationautoscaling to a direct dependency and bumps AWS SDK versions.
go.sum Updates checksums for the bumped AWS SDK modules.
drivers/autoscaling_group.go New driver implementation for scalable targets + scaling policy reconciliation.
drivers/autoscaling_group_test.go Unit tests for the new driver behavior.
Comments suppressed due to low confidence (2)

drivers/autoscaling_group.go:103

  • When ProviderID is empty, this falls back to using ref.Name as the ResourceIds filter. ResourceIds must be the AWS Application Auto Scaling resource_id format (e.g. service/cluster/service-name), so using the logical resource name is unlikely to work. Prefer requiring ProviderID (or another explicit identifier) rather than guessing here.
	out, err := d.client.DescribeScalableTargets(ctx, &applicationautoscaling.DescribeScalableTargetsInput{
		ServiceNamespace: aastypes.ServiceNamespace(ns),
		ResourceIds:      resourceIDFilter(resourceID, ref.Name),
		ScalableDimension: func() aastypes.ScalableDimension {

drivers/autoscaling_group.go:135

  • Same issue as Create: Update silently defaults min_capacity / max_capacity when not provided, which can unintentionally mutate capacity bounds. Validate required fields and capacity invariants before re-registering the target.
	minCap := int32(intProp(spec.Config, "min_capacity", 0))
	maxCap := int32(intProp(spec.Config, "max_capacity", 1))

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread drivers/autoscaling_group.go Outdated
Comment on lines +92 to +108

// If ProviderID is not set, we can only look up by namespace annotation stored in the ref.
// Fall back to the ref type context — caller must ensure ProviderID is populated for precise lookup.
if ns == "" {
// Use ref.Name as a best-effort resource_id if no ProviderID.
ns = "ecs" // default namespace for graceful degradation
}

out, err := d.client.DescribeScalableTargets(ctx, &applicationautoscaling.DescribeScalableTargetsInput{
ServiceNamespace: aastypes.ServiceNamespace(ns),
ResourceIds: resourceIDFilter(resourceID, ref.Name),
ScalableDimension: func() aastypes.ScalableDimension {
if dim != "" {
return aastypes.ScalableDimension(dim)
}
return ""
}(),
Comment thread drivers/autoscaling_group.go Outdated
Comment on lines +59 to +60
minCap := int32(intProp(spec.Config, "min_capacity", 0))
maxCap := int32(intProp(spec.Config, "max_capacity", 1))
Comment thread drivers/autoscaling_group.go Outdated
Comment on lines +201 to +205
// Compare the fields we surface in outputs.
want := map[string]any{
"min_capacity": intProp(desired.Config, "min_capacity", 0),
"max_capacity": intProp(desired.Config, "max_capacity", 1),
}
Comment on lines +404 to +411
case "TargetTrackingScaling":
in.PolicyType = aastypes.PolicyTypeTargetTrackingScaling
cfg := &aastypes.TargetTrackingScalingPolicyConfiguration{
TargetValue: awssdk.Float64(p.targetValue),
ScaleInCooldown: awssdk.Int32(p.scaleInCooldown),
ScaleOutCooldown: awssdk.Int32(p.scaleOutCooldown),
}
in.TargetTrackingScalingPolicyConfiguration = cfg
Comment on lines +412 to +417
case "StepScaling":
in.PolicyType = aastypes.PolicyTypeStepScaling
cfg := &aastypes.StepScalingPolicyConfiguration{
Cooldown: awssdk.Int32(p.scaleOutCooldown),
}
in.StepScalingPolicyConfiguration = cfg
// - role_arn (string, optional)
// - policies ([]any, optional) — each map with keys: policy_name, policy_type,
// target_value (TargetTracking), scale_in_cooldown, scale_out_cooldown (StepScaling)
type AutoScalingGroupDriver struct {
Comment thread plugin.json
"version": "0.1.0",
"author": "GoCodeAlone",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, and ACM resources",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, ACM, and AutoScaling Group resources",
Comment on lines +229 to +233
func TestAutoScalingGroupDriver_Read(t *testing.T) {
mock := &mockAutoScalingClient{
describeOut: &applicationautoscaling.DescribeScalableTargetsOutput{
ScalableTargets: []aastypes.ScalableTarget{
{
Comment on lines +138 to +142
func TestAutoScalingGroupDriver_Create_WithTargetTrackingPolicy(t *testing.T) {
mock := &mockAutoScalingClient{
registerOut: &applicationautoscaling.RegisterScalableTargetOutput{
ScalableTargetARN: awssdk.String("arn:aws:application-autoscaling:us-east-1:123:scalable-target/abc"),
},
Comment thread drivers/autoscaling_group.go Outdated
Comment on lines +79 to +82
targetARN := awssdk.ToString(out.ScalableTargetARN)
providerID := encodeProviderID(ns, resourceID, dim)

if err := d.syncPolicies(ctx, spec, ns, resourceID, dim, nil); err != nil {
…pec, stale-policy fetch

- Require ProviderID for Read/HealthCheck (no more ecs-namespace default guessing)
- Validate min_capacity/max_capacity presence and min<=max invariant in Create/Update
- Include policy_names fingerprint in Diff so policy changes trigger reconciliation
- TargetTrackingScaling: require predefined_metric_type (AWS rejects requests without it)
- StepScaling: require adjustment_type + step_adjustments (AWS rejects incomplete config)
- fetchLivePolicies now returns error; Delete aborts on fetch failure to avoid orphaned targets
- Create fetches live policies before syncPolicies for idempotent re-runs
- Embed noSensitiveKeys mixin instead of explicit SensitiveKeys method
- Tests: pass realistic ProviderID to Read/HealthCheck; capture and assert PutScalingPolicyInput fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.

Comment on lines +142 to +166
// Update re-registers the scalable target (idempotent) and reconciles scaling policies.
func (d *AutoScalingGroupDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) {
ns, resourceID, dim, err := requiredAutoScalingFields(spec)
if err != nil {
return nil, fmt.Errorf("autoscaling_group: update %q: %w", ref.Name, err)
}

minCap, maxCap, err := validateCapacity(spec.Config)
if err != nil {
return nil, fmt.Errorf("autoscaling_group: update %q: %w", ref.Name, err)
}
roleARN, _ := spec.Config["role_arn"].(string)

in := &applicationautoscaling.RegisterScalableTargetInput{
ServiceNamespace: aastypes.ServiceNamespace(ns),
ResourceId: awssdk.String(resourceID),
ScalableDimension: aastypes.ScalableDimension(dim),
MinCapacity: awssdk.Int32(minCap),
MaxCapacity: awssdk.Int32(maxCap),
}
if roleARN != "" {
in.RoleARN = awssdk.String(roleARN)
}

out, err := d.client.RegisterScalableTarget(ctx, in)
Comment on lines +220 to +261
// Diff computes whether the desired spec diverges from the current output.
// Compares capacity bounds and the sorted set of policy names.
func (d *AutoScalingGroupDriver) Diff(_ context.Context, desired interfaces.ResourceSpec, current *interfaces.ResourceOutput) (*interfaces.DiffResult, error) {
if current == nil {
return &interfaces.DiffResult{NeedsUpdate: true}, nil
}

// Build a deterministic policy-name fingerprint for comparison.
wantPolicies := desiredPolicyNames(desired.Config)
sort.Strings(wantPolicies)
wantPolicyKey := strings.Join(wantPolicies, ",")

// Extract current policy names from outputs.
var curPolicies []string
if pn, ok := current.Outputs["policy_names"]; ok {
switch v := pn.(type) {
case []string:
curPolicies = v
case []any:
for _, item := range v {
if s, ok := item.(string); ok {
curPolicies = append(curPolicies, s)
}
}
}
}
sort.Strings(curPolicies)
curPolicyKey := strings.Join(curPolicies, ",")

want := map[string]any{
"min_capacity": intProp(desired.Config, "min_capacity", 0),
"max_capacity": intProp(desired.Config, "max_capacity", 1),
"policy_names": wantPolicyKey,
}
currentForDiff := map[string]any{
"min_capacity": current.Outputs["min_capacity"],
"max_capacity": current.Outputs["max_capacity"],
"policy_names": curPolicyKey,
}
changes := diffOutputs(want, currentForDiff)
return &interfaces.DiffResult{NeedsUpdate: len(changes) > 0, Changes: changes}, nil
}
Comment thread drivers/autoscaling_group.go Outdated
Comment on lines +456 to +470
// parsePolicies extracts the policies slice from config.
func parsePolicies(config map[string]any) []policySpec {
raw, ok := config["policies"]
if !ok {
return nil
}
items, ok := raw.([]any)
if !ok {
return nil
}
var result []policySpec
for _, item := range items {
m, ok := item.(map[string]any)
if !ok {
continue
Comment on lines +472 to +537
p := policySpec{}
p.policyName, _ = m["policy_name"].(string)
p.policyType, _ = m["policy_type"].(string)
if tv, ok := m["target_value"].(float64); ok {
p.targetValue = tv
}
p.predefinedMetricType, _ = m["predefined_metric_type"].(string)
p.scaleInCooldown = int32(intProp(m, "scale_in_cooldown", 300))
p.scaleOutCooldown = int32(intProp(m, "scale_out_cooldown", 300))
p.adjustmentType, _ = m["adjustment_type"].(string)
p.cooldown = int32(intProp(m, "cooldown", 300))

// Parse step_adjustments for StepScaling.
if saRaw, ok := m["step_adjustments"]; ok {
if saItems, ok := saRaw.([]any); ok {
for _, saItem := range saItems {
saMap, ok := saItem.(map[string]any)
if !ok {
continue
}
sa := stepAdjustment{
scalingAdjustment: int32(intProp(saMap, "scaling_adjustment", 0)),
}
if lb, ok := saMap["metric_interval_lower_bound"].(float64); ok {
sa.metricIntervalLowerBound = awssdk.Float64(lb)
}
if ub, ok := saMap["metric_interval_upper_bound"].(float64); ok {
sa.metricIntervalUpperBound = awssdk.Float64(ub)
}
p.stepAdjustments = append(p.stepAdjustments, sa)
}
}
}

if p.policyName != "" {
result = append(result, p)
}
}
return result
}

// putPolicy calls PutScalingPolicy for a single policySpec.
// Both TargetTrackingScaling and StepScaling are supported with their required fields.
func (d *AutoScalingGroupDriver) putPolicy(ctx context.Context, ns, resourceID, dim string, p policySpec) error {
in := &applicationautoscaling.PutScalingPolicyInput{
PolicyName: awssdk.String(p.policyName),
ServiceNamespace: aastypes.ServiceNamespace(ns),
ResourceId: awssdk.String(resourceID),
ScalableDimension: aastypes.ScalableDimension(dim),
}

switch p.policyType {
case "TargetTrackingScaling":
if p.predefinedMetricType == "" {
return fmt.Errorf("put policy %q: predefined_metric_type is required for TargetTrackingScaling (e.g., ECSServiceAverageCPUUtilization)", p.policyName)
}
in.PolicyType = aastypes.PolicyTypeTargetTrackingScaling
cfg := &aastypes.TargetTrackingScalingPolicyConfiguration{
TargetValue: awssdk.Float64(p.targetValue),
PredefinedMetricSpecification: &aastypes.PredefinedMetricSpecification{
PredefinedMetricType: aastypes.MetricType(p.predefinedMetricType),
},
ScaleInCooldown: awssdk.Int32(p.scaleInCooldown),
ScaleOutCooldown: awssdk.Int32(p.scaleOutCooldown),
}
in.TargetTrackingScalingPolicyConfiguration = cfg
Comment on lines +112 to +140
// Read describes the scalable target and its policies.
// ProviderID is required (format: "namespace|resource_id|scalable_dimension").
func (d *AutoScalingGroupDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) {
ns, resourceID, dim := decodeProviderID(ref.ProviderID)
if ns == "" {
return nil, fmt.Errorf("autoscaling_group: read %q: ProviderID is required (format: namespace|resource_id|dimension)", ref.Name)
}

out, err := d.client.DescribeScalableTargets(ctx, &applicationautoscaling.DescribeScalableTargetsInput{
ServiceNamespace: aastypes.ServiceNamespace(ns),
ResourceIds: []string{resourceID},
ScalableDimension: aastypes.ScalableDimension(dim),
})
if err != nil {
return nil, fmt.Errorf("autoscaling_group: describe %q: %w", ref.Name, err)
}
if len(out.ScalableTargets) == 0 {
return nil, fmt.Errorf("autoscaling_group: scalable target %q not found", ref.Name)
}
target := out.ScalableTargets[0]

policyNames := d.readPolicyNames(ctx, target)
minCap := int(awssdk.ToInt32(target.MinCapacity))
maxCap := int(awssdk.ToInt32(target.MaxCapacity))
providerID := encodeProviderID(string(target.ServiceNamespace), awssdk.ToString(target.ResourceId), string(target.ScalableDimension))
targetARN := awssdk.ToString(target.ScalableTargetARN)

return d.buildOutput(ref.Name, targetARN, providerID, minCap, maxCap, policyNames), nil
}
Comment on lines +281 to +294
func validateCapacity(config map[string]any) (minCap, maxCap int32, err error) {
minRaw, minOK := config["min_capacity"]
maxRaw, maxOK := config["max_capacity"]
if !minOK {
return 0, 0, fmt.Errorf("min_capacity is required")
}
if !maxOK {
return 0, 0, fmt.Errorf("max_capacity is required")
}
minI := intProp(config, "min_capacity", -1)
maxI := intProp(config, "max_capacity", -1)
_ = minRaw
_ = maxRaw
if minI < 0 {
Comment thread plugin.json
Comment on lines 4 to +10
"author": "GoCodeAlone",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, and ACM resources",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, ACM, and AutoScaling Group resources",
"license": "MIT",
"type": "external",
"tier": "community",
"minEngineVersion": "0.19.0",
"keywords": ["aws", "iac", "infrastructure", "ecs", "eks", "rds", "vpc", "s3"],
"keywords": ["aws", "iac", "infrastructure", "ecs", "eks", "rds", "vpc", "s3", "autoscaling"],
…rsePolicies, target_value

- Remove dead code in validateCapacity (minRaw/maxRaw assigned then discarded)
- Update(): validate ref.ProviderID matches spec identity; return clear error on mismatch
  to prevent orphaning the previously managed scalable target
- Diff(): include provider_id in comparison to detect identity drift (ns/resource_id/dim changes)
- parsePolicies(): return error on malformed input (wrong type / non-map entry) so
  syncPolicies fails safely instead of silently treating malformed input as "no policies"
  and destructively deleting all live policies
- target_value: accept int/int64 in addition to float64 (common in YAML decoding);
  validate > 0 before PutScalingPolicy
- readPolicyNames(): propagate DescribeScalingPolicies error so Read/HealthCheck callers
  can distinguish "target exists" from "policy enumeration failed"
- Tests: add Diff identity-drift, Update identity-mismatch, int target_value, and
  malformed-policies-type cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (5)

drivers/autoscaling_group.go:245

  • desiredPolicyNames() discards parsePolicies() errors. As a result, malformed policy specs (e.g., policies not being a list / wrong element types) can be silently treated as “no policies” during Diff, producing misleading plans. Consider having Diff call parsePolicies() directly and return the parse error (or have desiredPolicyNames return ([]string, error)).
	// Build a deterministic policy-name fingerprint for comparison.
	wantPolicies := desiredPolicyNames(desired.Config)
	sort.Strings(wantPolicies)
	wantPolicyKey := strings.Join(wantPolicies, ",")

drivers/autoscaling_group.go:414

  • The comment says malformed policy entries are skipped, but desiredPolicyNames() currently calls parsePolicies(), which returns an error on malformed entries/types. Either adjust the comment to match the current behavior or change desiredPolicyNames/parsePolicies to actually skip invalid entries when used for fingerprinting.
// desiredPolicyNames returns the policy_name values from the spec config.
// Malformed policy entries are skipped to allow safe fingerprinting; callers
// that need strict validation use parsePolicies directly.
func desiredPolicyNames(config map[string]any) []string {
	policies, _ := parsePolicies(config)
	names := make([]string, 0, len(policies))

drivers/autoscaling_group.go:536

  • StepScaling step_adjustments: scaling_adjustment is documented/treated as required, but parsePolicies defaults it to 0 when missing. This can silently create a no-op scaling step. Consider explicitly validating that scaling_adjustment is present and of a numeric type, returning a clear error if missing/invalid.
			for j, saItem := range saItems {
				saMap, ok := saItem.(map[string]any)
				if !ok {
					return nil, fmt.Errorf("policies[%d].step_adjustments[%d] must be a map, got %T", i, j, saItem)
				}
				sa := stepAdjustment{
					scalingAdjustment: int32(intProp(saMap, "scaling_adjustment", 0)),
				}

drivers/autoscaling_group.go:542

  • step_adjustments metric_interval_lower_bound / metric_interval_upper_bound are only parsed when the value is float64. When configs are decoded from YAML/JSON, these bounds can commonly be int/int64, which would be ignored and change scaling behavior. Consider accepting int/int64 (and possibly json.Number) the same way target_value is handled.
				}
				if lb, ok := saMap["metric_interval_lower_bound"].(float64); ok {
					sa.metricIntervalLowerBound = awssdk.Float64(lb)
				}
				if ub, ok := saMap["metric_interval_upper_bound"].(float64); ok {
					sa.metricIntervalUpperBound = awssdk.Float64(ub)
				}

drivers/autoscaling_group.go:550

  • parsePolicies() silently drops policy entries that are missing/empty policy_name (only appends when policyName != ""). In syncPolicies(), an omitted name can effectively make the desired policy set empty and trigger deletion of all live policies. Consider treating empty policy_name as a validation error when the policies key is present to avoid accidental destructive reconciliation.
		if p.policyName != "" {
			result = append(result, p)
		}
	}

Comment on lines +235 to +245
// Diff computes whether the desired spec diverges from the current output.
// Compares capacity bounds and the sorted set of policy names.
func (d *AutoScalingGroupDriver) Diff(_ context.Context, desired interfaces.ResourceSpec, current *interfaces.ResourceOutput) (*interfaces.DiffResult, error) {
if current == nil {
return &interfaces.DiffResult{NeedsUpdate: true}, nil
}

// Build a deterministic policy-name fingerprint for comparison.
wantPolicies := desiredPolicyNames(desired.Config)
sort.Strings(wantPolicies)
wantPolicyKey := strings.Join(wantPolicies, ",")
Comment thread plugin.json
"version": "0.1.0",
"author": "GoCodeAlone",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, and ACM resources",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, ACM, and AutoScaling Group resources",
…g fingerprint

- Diff now detects policy parameter changes (target_value, cooldowns, step_adjustments)
  by computing a deterministic policy_fingerprint from desired config and comparing
  against the stored fingerprint in current.Outputs; same-named policy with changed
  target_value now correctly triggers NeedsUpdate=true
- buildOutputWithConfig stores policy_fingerprint in outputs for round-trip comparison
- Create and Update use buildOutputWithConfig to persist the fingerprint
- Add TestAutoScalingGroupDriver_Diff_PolicyConfigChange to cover this gap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@intel352 intel352 requested a review from Copilot May 13, 2026 11:23
@intel352 intel352 merged commit ac14213 into main May 13, 2026
9 checks passed
@intel352 intel352 deleted the feat/issue-9-autoscaling-group branch May 13, 2026 11:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

drivers/autoscaling_group.go:602

  • parsePolicies silently drops policy entries that omit policy_name (it just skips appending when policyName == ""). Since policy_name is documented as required, this can lead to an apply that unexpectedly deletes all live policies (desiredNames becomes empty) or silently ignores user intent. Treat missing/empty policy_name as a validation error when the policies list is provided (and include the index in the error).
		if p.policyName != "" {
			result = append(result, p)
		}
	}

drivers/autoscaling_group.go:594

  • step_adjustments parsing only accepts float64 for metric_interval_lower_bound / metric_interval_upper_bound. In practice, YAML/JSON decoding often yields int or int64 for whole numbers (e.g., 0), so bounds may be silently ignored and the policy sent to AWS will be wrong. Accept int/int64 (and possibly json.Number) for these fields similar to target_value handling, or error on unsupported numeric types.
				if lb, ok := saMap["metric_interval_lower_bound"].(float64); ok {
					sa.metricIntervalLowerBound = awssdk.Float64(lb)
				}
				if ub, ok := saMap["metric_interval_upper_bound"].(float64); ok {
					sa.metricIntervalUpperBound = awssdk.Float64(ub)
				}

Comment on lines +416 to +423
case "StepScaling":
steps := make([]string, len(p.stepAdjustments))
for i, sa := range p.stepAdjustments {
steps[i] = fmt.Sprintf("%d", sa.scalingAdjustment)
}
parts = append(parts, fmt.Sprintf("%s:SS:%s:%d:%s",
p.policyName, p.adjustmentType, p.cooldown, strings.Join(steps, "+")))
default:
Comment on lines +433 to +444
out, err := d.client.DescribeScalingPolicies(ctx, &applicationautoscaling.DescribeScalingPoliciesInput{
ServiceNamespace: aastypes.ServiceNamespace(ns),
ResourceId: awssdk.String(resourceID),
ScalableDimension: aastypes.ScalableDimension(dim),
})
if err != nil {
return nil, err
}
if out == nil {
return nil, nil
}
return out.ScalingPolicies, nil
Comment thread plugin.json
"version": "0.1.0",
"author": "GoCodeAlone",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, and ACM resources",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, ACM, and AutoScaling Group resources",
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.

Add infra.autoscaling_group resource type — blocker for workflow#653 Phase 1

2 participants