feat(autoscaling_group): add infra.autoscaling_group driver#10
Conversation
…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>
There was a problem hiding this comment.
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_groupdriver inAWSProviderand advertise it viaCapabilities(). - Introduce
drivers/autoscaling_group.goimplementing 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
applicationautoscalingdirectly.
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
ProviderIDis empty, this falls back to usingref.Nameas theResourceIdsfilter.ResourceIdsmust be the AWS Application Auto Scalingresource_idformat (e.g.service/cluster/service-name), so using the logical resource name is unlikely to work. Prefer requiringProviderID(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:Updatesilently defaultsmin_capacity/max_capacitywhen 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.
|
|
||
| // 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 "" | ||
| }(), |
| minCap := int32(intProp(spec.Config, "min_capacity", 0)) | ||
| maxCap := int32(intProp(spec.Config, "max_capacity", 1)) |
| // 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), | ||
| } |
| 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 |
| 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 { |
| "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", |
| func TestAutoScalingGroupDriver_Read(t *testing.T) { | ||
| mock := &mockAutoScalingClient{ | ||
| describeOut: &applicationautoscaling.DescribeScalableTargetsOutput{ | ||
| ScalableTargets: []aastypes.ScalableTarget{ | ||
| { |
| 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"), | ||
| }, |
| 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>
| // 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) |
| // 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 | ||
| } |
| // 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 |
| 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 |
| // 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 | ||
| } |
| 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 { |
| "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>
There was a problem hiding this comment.
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)
}
}
| // 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, ",") |
| "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>
There was a problem hiding this comment.
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)
}
| 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: |
| 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 |
| "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", |
Summary
infra.autoscaling_groupresource driver wrapping AWS Application Auto Scaling — covers ECS services, DynamoDB, RDS Aurora replicas, AppStream, and any Application Auto Scaling namespace.TargetTrackingScalingandStepScalingpolicy types with separate configuration shapes for each.RegisterScalableTarget) and reconciles policies: stale live policies absent from the desired spec are deleted before upserting the desired ones.applicationautoscalingfrom indirect to direct dependency; plugs the driver intoprovider.registerDriversandCapabilities, and adds"infra.autoscaling_group"toplugin.json.Test plan
go test ./drivers -run TestAutoScalingGroup -vconfirmed build-fail before impl)go build ./...cleango test -race ./...clean — all packages passNotes
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