Skip to content

Commit 3a3cb08

Browse files
committed
chore: import autoscaler resource
1 parent c99571a commit 3a3cb08

File tree

2 files changed

+244
-7
lines changed

2 files changed

+244
-7
lines changed

castai/resource_autoscaler.go

Lines changed: 238 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ func resourceAutoscaler() *schema.Resource {
7272
UpdateContext: resourceCastaiAutoscalerUpdate,
7373
DeleteContext: resourceCastaiAutoscalerDelete,
7474
CustomizeDiff: resourceCastaiAutoscalerDiff,
75-
Description: "CAST AI autoscaler resource to manage autoscaler settings",
75+
Importer: &schema.ResourceImporter{
76+
StateContext: schema.ImportStatePassthroughContext,
77+
},
78+
Description: "CAST AI autoscaler resource to manage autoscaler settings",
7679

7780
Timeouts: &schema.ResourceTimeout{
7881
Create: schema.DefaultTimeout(2 * time.Minute),
@@ -639,9 +642,41 @@ func readAutoscalerPolicies(ctx context.Context, data *schema.ResourceData, meta
639642
return err
640643
}
641644

642-
err = data.Set(FieldAutoscalerPolicies, string(currentPolicies))
645+
// Set cluster_id if not already set (e.g., during import)
646+
if _, ok := data.GetOk(FieldClusterId); !ok {
647+
if err := data.Set(FieldClusterId, clusterId); err != nil {
648+
log.Printf("[ERROR] Failed to set cluster_id field: %v", err)
649+
return err
650+
}
651+
}
652+
653+
// Filter out API-managed fields before storing in autoscaler_policies
654+
// to prevent drift detection on fields that change independently of user config
655+
var policyMap map[string]interface{}
656+
if err := json.Unmarshal(currentPolicies, &policyMap); err != nil {
657+
return fmt.Errorf("unmarshaling policies: %w", err)
658+
}
659+
delete(policyMap, "defaultNodeTemplateVersion")
660+
661+
filteredPolicies, err := json.Marshal(policyMap)
662+
if err != nil {
663+
return fmt.Errorf("marshaling filtered policies: %w", err)
664+
}
665+
666+
if err := data.Set(FieldAutoscalerPolicies, string(filteredPolicies)); err != nil {
667+
log.Printf("[ERROR] Failed to set autoscaler_policies field: %v", err)
668+
return err
669+
}
670+
671+
// Populate autoscaler_settings from API response for import and drift detection
672+
autoscalerSettings, err := flattenAutoscalerPolicy(currentPolicies)
643673
if err != nil {
644-
log.Printf("[ERROR] Failed to set field: %v", err)
674+
log.Printf("[ERROR] Failed to flatten autoscaler policy: %v", err)
675+
return err
676+
}
677+
678+
if err := data.Set(FieldAutoscalerSettings, autoscalerSettings); err != nil {
679+
log.Printf("[ERROR] Failed to set autoscaler_settings field: %v", err)
645680
return err
646681
}
647682

@@ -650,11 +685,16 @@ func readAutoscalerPolicies(ctx context.Context, data *schema.ResourceData, meta
650685

651686
func getClusterId(data types.ResourceProvider) string {
652687
value, found := data.GetOk(FieldClusterId)
653-
if !found {
654-
return ""
688+
if found {
689+
return value.(string)
655690
}
656691

657-
return value.(string)
692+
// Fallback to resource ID for import scenarios where cluster_id attribute isn't set yet
693+
if rd, ok := data.(*schema.ResourceData); ok {
694+
return rd.Id()
695+
}
696+
697+
return ""
658698
}
659699

660700
func getChangedPolicies(ctx context.Context, data types.ResourceProvider, meta interface{}, clusterId string) ([]byte, error) {
@@ -703,7 +743,20 @@ func getChangedPolicies(ctx context.Context, data types.ResourceProvider, meta i
703743
return nil, fmt.Errorf("failed to merge policies: %v", err)
704744
}
705745

706-
return normalizeJSON(policies)
746+
// Filter out API-managed fields to prevent drift detection
747+
// on fields that change independently of user config
748+
var policyMap map[string]interface{}
749+
if err := json.Unmarshal(policies, &policyMap); err != nil {
750+
return nil, fmt.Errorf("unmarshaling merged policies: %w", err)
751+
}
752+
delete(policyMap, "defaultNodeTemplateVersion")
753+
754+
filteredPolicies, err := json.Marshal(policyMap)
755+
if err != nil {
756+
return nil, fmt.Errorf("marshaling filtered policies: %w", err)
757+
}
758+
759+
return normalizeJSON(filteredPolicies)
707760
}
708761

709762
func validateAutoscalerPolicyJSON() schema.SchemaValidateDiagFunc {
@@ -775,3 +828,181 @@ func toAutoscalerPolicy(data types.ResourceProvider) (*types.AutoscalerPolicy, e
775828

776829
return &policy, nil
777830
}
831+
832+
// flattenAutoscalerPolicy converts API JSON response to autoscaler_settings schema format.
833+
// This is the reverse of toAutoscalerPolicy and is used during Read/Import operations.
834+
func flattenAutoscalerPolicy(policyJSON []byte) ([]map[string]interface{}, error) {
835+
if len(policyJSON) == 0 {
836+
return nil, nil
837+
}
838+
839+
var policy types.AutoscalerPolicy
840+
if err := json.Unmarshal(policyJSON, &policy); err != nil {
841+
return nil, fmt.Errorf("unmarshaling policy JSON: %w", err)
842+
}
843+
844+
settings := map[string]interface{}{
845+
FieldEnabled: policy.Enabled,
846+
FieldIsScopedMode: policy.IsScopedMode,
847+
FieldNodeTemplatesPartialMatchingEnabled: policy.NodeTemplatesPartialMatching,
848+
}
849+
850+
if policy.UnschedulablePods != nil {
851+
unschedulablePods := map[string]interface{}{
852+
FieldEnabled: policy.UnschedulablePods.Enabled,
853+
}
854+
855+
if policy.UnschedulablePods.Headroom != nil {
856+
h := policy.UnschedulablePods.Headroom
857+
// Only include if has non-zero percentages (meaningful values)
858+
if h.CPUPercentage > 0 || h.MemoryPercentage > 0 {
859+
unschedulablePods[FieldHeadroom] = []map[string]interface{}{
860+
{
861+
FieldEnabled: h.Enabled,
862+
FieldCPUPercentage: h.CPUPercentage,
863+
FieldMemoryPercentage: h.MemoryPercentage,
864+
},
865+
}
866+
}
867+
}
868+
869+
if policy.UnschedulablePods.HeadroomSpot != nil {
870+
hs := policy.UnschedulablePods.HeadroomSpot
871+
// Only include if has non-zero percentages (meaningful values)
872+
if hs.CPUPercentage > 0 || hs.MemoryPercentage > 0 {
873+
unschedulablePods[FieldHeadroomSpot] = []map[string]interface{}{
874+
{
875+
FieldEnabled: hs.Enabled,
876+
FieldCPUPercentage: hs.CPUPercentage,
877+
FieldMemoryPercentage: hs.MemoryPercentage,
878+
},
879+
}
880+
}
881+
}
882+
883+
if policy.UnschedulablePods.NodeConstraints != nil {
884+
nc := policy.UnschedulablePods.NodeConstraints
885+
// Only include if user enabled the feature or set minimum constraints.
886+
// Max values are always returned by API as defaults, so they're not meaningful
887+
// indicators of user configuration. Min values > 0 indicate user set constraints.
888+
if nc.Enabled || nc.MinCPUCores > 0 || nc.MinRAMMiB > 0 {
889+
unschedulablePods[FieldNodeConstraints] = []map[string]interface{}{
890+
{
891+
FieldEnabled: nc.Enabled,
892+
FieldMinCPUCores: nc.MinCPUCores,
893+
FieldMaxCPUCores: nc.MaxCPUCores,
894+
FieldMinRAMMiB: nc.MinRAMMiB,
895+
FieldMaxRAMMiB: nc.MaxRAMMiB,
896+
},
897+
}
898+
}
899+
}
900+
901+
if policy.UnschedulablePods.CustomInstances != nil {
902+
unschedulablePods[FieldCustomInstancesEnabled] = *policy.UnschedulablePods.CustomInstances
903+
}
904+
905+
// Only include PodPinner if enabled
906+
if policy.UnschedulablePods.PodPinner != nil && policy.UnschedulablePods.PodPinner.Enabled {
907+
unschedulablePods[FieldPodPinner] = []map[string]interface{}{
908+
{
909+
FieldEnabled: policy.UnschedulablePods.PodPinner.Enabled,
910+
},
911+
}
912+
}
913+
914+
settings[FieldUnschedulablePods] = []map[string]interface{}{unschedulablePods}
915+
}
916+
917+
if policy.ClusterLimits != nil {
918+
clusterLimits := map[string]interface{}{
919+
FieldEnabled: policy.ClusterLimits.Enabled,
920+
}
921+
922+
if policy.ClusterLimits.CPU != nil {
923+
clusterLimits[FieldCPU] = []map[string]interface{}{
924+
{
925+
FieldMinCores: policy.ClusterLimits.CPU.MinCores,
926+
FieldMaxCores: policy.ClusterLimits.CPU.MaxCores,
927+
},
928+
}
929+
}
930+
931+
settings[FieldClusterLimits] = []map[string]interface{}{clusterLimits}
932+
}
933+
934+
if policy.SpotInstances != nil {
935+
si := policy.SpotInstances
936+
// Only include SpotInstances if it has meaningful settings
937+
hasSpotBackups := si.SpotBackups != nil && si.SpotBackups.Enabled
938+
hasInterruptionPredictions := si.SpotInterruptionPredictions != nil && si.SpotInterruptionPredictions.Enabled
939+
if si.Enabled || si.MaxReclaimRate > 0 || si.SpotDiversityEnabled || hasSpotBackups || hasInterruptionPredictions {
940+
spotInstances := map[string]interface{}{
941+
FieldEnabled: si.Enabled,
942+
FieldMaxReclaimRate: si.MaxReclaimRate,
943+
FieldSpotDiversityEnabled: si.SpotDiversityEnabled,
944+
FieldSpotDiversityPriceIncreaseLimit: si.SpotDiversityPriceIncrease,
945+
}
946+
947+
// Only include SpotBackups if enabled
948+
if hasSpotBackups {
949+
spotInstances[FieldSpotBackups] = []map[string]interface{}{
950+
{
951+
FieldEnabled: si.SpotBackups.Enabled,
952+
FieldSpotBackupRestoreRateSeconds: si.SpotBackups.SpotBackupRestoreRateSeconds,
953+
},
954+
}
955+
}
956+
957+
// Only include SpotInterruptionPredictions if enabled
958+
if hasInterruptionPredictions {
959+
spotInstances[FieldSpotInterruptionPredictions] = []map[string]interface{}{
960+
{
961+
FieldEnabled: si.SpotInterruptionPredictions.Enabled,
962+
FieldSpotInterruptionPredictionsType: si.SpotInterruptionPredictions.SpotInterruptionPredictionsType,
963+
},
964+
}
965+
}
966+
967+
settings[FieldSpotInstances] = []map[string]interface{}{spotInstances}
968+
}
969+
}
970+
971+
if policy.NodeDownscaler != nil {
972+
nodeDownscaler := map[string]interface{}{
973+
FieldEnabled: policy.NodeDownscaler.Enabled,
974+
}
975+
976+
if policy.NodeDownscaler.EmptyNodes != nil {
977+
nodeDownscaler[FieldEmptyNodes] = []map[string]interface{}{
978+
{
979+
FieldEnabled: policy.NodeDownscaler.EmptyNodes.Enabled,
980+
FieldDelaySeconds: policy.NodeDownscaler.EmptyNodes.DelaySeconds,
981+
},
982+
}
983+
}
984+
985+
// Only include Evictor if enabled or has non-default settings
986+
if policy.NodeDownscaler.Evictor != nil {
987+
e := policy.NodeDownscaler.Evictor
988+
if e.Enabled || e.DryRun || e.AggressiveMode || e.ScopedMode {
989+
nodeDownscaler[FieldEvictor] = []map[string]interface{}{
990+
{
991+
FieldEnabled: e.Enabled,
992+
FieldEvictorDryRun: e.DryRun,
993+
FieldEvictorAggressiveMode: e.AggressiveMode,
994+
FieldEvictorScopedMode: e.ScopedMode,
995+
FieldEvictorCycleInterval: e.CycleInterval,
996+
FieldEvictorNodeGracePeriodMinutes: e.NodeGracePeriodMinutes,
997+
FieldEvictorPodEvictionFailureBackOffInterval: e.PodEvictionFailureBackOffInterval,
998+
FieldEvictorIgnorePodDisruptionBudgets: e.IgnorePodDisruptionBudgets,
999+
},
1000+
}
1001+
}
1002+
}
1003+
1004+
settings[FieldNodeDownscaler] = []map[string]interface{}{nodeDownscaler}
1005+
}
1006+
1007+
return []map[string]interface{}{settings}, nil
1008+
}

castai/resource_autoscaler_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,12 @@ func TestAccEKS_ResourceAutoscaler_basic(t *testing.T) {
10131013
resource.TestCheckResourceAttr("castai_autoscaler.test", "autoscaler_settings.0.node_downscaler.0.empty_nodes.0.delay_seconds", "300"),
10141014
),
10151015
},
1016+
// Step 3: Test import
1017+
{
1018+
ResourceName: "castai_autoscaler.test",
1019+
ImportState: true,
1020+
ImportStateVerify: true,
1021+
},
10161022
},
10171023
ExternalProviders: map[string]resource.ExternalProvider{
10181024
"aws": {

0 commit comments

Comments
 (0)