@@ -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
651686func 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
660700func 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
709762func 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+ }
0 commit comments