@@ -11,6 +11,7 @@ import (
1111 "time"
1212
1313 jsonpatch "github.com/evanphx/json-patch"
14+ "github.com/google/uuid"
1415 "github.com/hashicorp/go-cty/cty"
1516 "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1617 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -72,7 +73,10 @@ func resourceAutoscaler() *schema.Resource {
7273 UpdateContext : resourceCastaiAutoscalerUpdate ,
7374 DeleteContext : resourceCastaiAutoscalerDelete ,
7475 CustomizeDiff : resourceCastaiAutoscalerDiff ,
75- Description : "CAST AI autoscaler resource to manage autoscaler settings" ,
76+ Importer : & schema.ResourceImporter {
77+ StateContext : autoscalerStateImporter ,
78+ },
79+ Description : "CAST AI autoscaler resource to manage autoscaler settings" ,
7680
7781 Timeouts : & schema.ResourceTimeout {
7882 Create : schema .DefaultTimeout (2 * time .Minute ),
@@ -97,6 +101,7 @@ func resourceAutoscaler() *schema.Resource {
97101 Type : schema .TypeString ,
98102 Computed : true ,
99103 Description : "computed value to store full policies configuration" ,
104+ Deprecated : "This field is deprecated and will be removed in the next major version. Use autoscaler_settings to configure and manage autoscaler policies." ,
100105 },
101106 FieldAutoscalerSettings : {
102107 Type : schema .TypeList ,
@@ -513,6 +518,212 @@ func resourceCastaiAutoscalerDelete(ctx context.Context, data *schema.ResourceDa
513518 return nil
514519}
515520
521+ func autoscalerStateImporter (ctx context.Context , d * schema.ResourceData , meta interface {}) ([]* schema.ResourceData , error ) {
522+ clusterID := d .Id ()
523+
524+ // Validate cluster ID is a UUID
525+ if _ , err := uuid .Parse (clusterID ); err != nil {
526+ return nil , fmt .Errorf ("expected cluster_id to be a valid UUID, got: %q" , clusterID )
527+ }
528+
529+ // Set the cluster_id field
530+ if err := d .Set (FieldClusterId , clusterID ); err != nil {
531+ return nil , fmt .Errorf ("setting cluster_id: %w" , err )
532+ }
533+
534+ // Fetch current policies from API
535+ client := meta .(* ProviderConfig ).api
536+ currentPolicies , err := getCurrentPolicies (ctx , client , clusterID )
537+ if err != nil {
538+ return nil , fmt .Errorf ("fetching autoscaler policies for cluster %s: %w" , clusterID , err )
539+ }
540+
541+ // Set the computed autoscaler_policies field
542+ if err := d .Set (FieldAutoscalerPolicies , string (currentPolicies )); err != nil {
543+ return nil , fmt .Errorf ("setting autoscaler_policies: %w" , err )
544+ }
545+
546+ // Convert API response to autoscaler_settings structure
547+ settings , err := flattenAutoscalerSettings (currentPolicies )
548+ if err != nil {
549+ return nil , fmt .Errorf ("converting policies to autoscaler_settings: %w" , err )
550+ }
551+
552+ if settings != nil {
553+ if err := d .Set (FieldAutoscalerSettings , settings ); err != nil {
554+ return nil , fmt .Errorf ("setting autoscaler_settings: %w" , err )
555+ }
556+ }
557+
558+ return []* schema.ResourceData {d }, nil
559+ }
560+
561+ // flattenAutoscalerSettings converts API JSON response to autoscaler_settings Terraform structure.
562+ // It omits deprecated fields to encourage users to migrate to castai_node_template.
563+ func flattenAutoscalerSettings (policiesJSON []byte ) ([]map [string ]interface {}, error ) {
564+ var apiPolicy map [string ]interface {}
565+ if err := json .Unmarshal (policiesJSON , & apiPolicy ); err != nil {
566+ return nil , fmt .Errorf ("unmarshaling policies JSON: %w" , err )
567+ }
568+
569+ settings := make (map [string ]interface {})
570+
571+ // Top-level fields
572+ if v , ok := apiPolicy ["enabled" ]; ok {
573+ settings [FieldEnabled ] = v
574+ }
575+ if v , ok := apiPolicy ["isScopedMode" ]; ok {
576+ settings [FieldIsScopedMode ] = v
577+ }
578+ if v , ok := apiPolicy ["nodeTemplatesPartialMatchingEnabled" ]; ok {
579+ settings [FieldNodeTemplatesPartialMatchingEnabled ] = v
580+ }
581+
582+ // Unschedulable Pods (excluding deprecated fields: headroom, headroomSpot, nodeConstraints, customInstancesEnabled)
583+ if unschedulablePods , ok := apiPolicy ["unschedulablePods" ].(map [string ]interface {}); ok {
584+ up := flattenUnschedulablePods (unschedulablePods )
585+ if len (up ) > 0 {
586+ settings [FieldUnschedulablePods ] = []interface {}{up }
587+ }
588+ }
589+
590+ // Cluster Limits
591+ if clusterLimits , ok := apiPolicy ["clusterLimits" ].(map [string ]interface {}); ok {
592+ cl := flattenClusterLimits (clusterLimits )
593+ if len (cl ) > 0 {
594+ settings [FieldClusterLimits ] = []interface {}{cl }
595+ }
596+ }
597+
598+ // Node Downscaler (excluding spot_instances which is deprecated at top level)
599+ if nodeDownscaler , ok := apiPolicy ["nodeDownscaler" ].(map [string ]interface {}); ok {
600+ nd := flattenNodeDownscaler (nodeDownscaler )
601+ if len (nd ) > 0 {
602+ settings [FieldNodeDownscaler ] = []interface {}{nd }
603+ }
604+ }
605+
606+ // Note: Omitting spotInstances entirely (deprecated)
607+
608+ return []map [string ]interface {}{settings }, nil
609+ }
610+
611+ // flattenUnschedulablePods converts the unschedulablePods API response to Terraform structure.
612+ // Deprecated fields (headroom, headroomSpot, nodeConstraints, customInstancesEnabled) are omitted.
613+ // Only includes pod_pinner if enabled to avoid state mismatch with configs that don't specify it.
614+ func flattenUnschedulablePods (unschedulablePods map [string ]interface {}) map [string ]interface {} {
615+ up := make (map [string ]interface {})
616+
617+ if v , ok := unschedulablePods ["enabled" ]; ok {
618+ up [FieldEnabled ] = v
619+ }
620+
621+ // Note: Omitting headroom, headroomSpot, nodeConstraints, customInstancesEnabled (deprecated)
622+
623+ // Pod Pinner - only include if enabled to avoid state mismatch
624+ if podPinner , ok := unschedulablePods ["podPinner" ].(map [string ]interface {}); ok {
625+ if enabled , ok := podPinner ["enabled" ].(bool ); ok && enabled {
626+ pp := map [string ]interface {}{
627+ FieldEnabled : true ,
628+ }
629+ up [FieldPodPinner ] = []interface {}{pp }
630+ }
631+ }
632+
633+ return up
634+ }
635+
636+ // flattenClusterLimits converts the clusterLimits API response to Terraform structure.
637+ func flattenClusterLimits (clusterLimits map [string ]interface {}) map [string ]interface {} {
638+ cl := make (map [string ]interface {})
639+
640+ if v , ok := clusterLimits ["enabled" ]; ok {
641+ cl [FieldEnabled ] = v
642+ }
643+
644+ if cpu , ok := clusterLimits ["cpu" ].(map [string ]interface {}); ok {
645+ cpuMap := make (map [string ]interface {})
646+ if v , ok := cpu ["minCores" ]; ok {
647+ cpuMap [FieldMinCores ] = v
648+ }
649+ if v , ok := cpu ["maxCores" ]; ok {
650+ cpuMap [FieldMaxCores ] = v
651+ }
652+ if len (cpuMap ) > 0 {
653+ cl [FieldCPU ] = []interface {}{cpuMap }
654+ }
655+ }
656+
657+ return cl
658+ }
659+
660+ // flattenNodeDownscaler converts the nodeDownscaler API response to Terraform structure.
661+ // Only includes evictor if enabled to avoid state mismatch with configs that don't specify it.
662+ func flattenNodeDownscaler (nodeDownscaler map [string ]interface {}) map [string ]interface {} {
663+ nd := make (map [string ]interface {})
664+
665+ if v , ok := nodeDownscaler ["enabled" ]; ok {
666+ nd [FieldEnabled ] = v
667+ }
668+
669+ if emptyNodes , ok := nodeDownscaler ["emptyNodes" ].(map [string ]interface {}); ok {
670+ en := make (map [string ]interface {})
671+ if v , ok := emptyNodes ["enabled" ]; ok {
672+ en [FieldEnabled ] = v
673+ }
674+ if v , ok := emptyNodes ["delaySeconds" ]; ok {
675+ en [FieldDelaySeconds ] = v
676+ }
677+ if len (en ) > 0 {
678+ nd [FieldEmptyNodes ] = []interface {}{en }
679+ }
680+ }
681+
682+ // Evictor - only include if enabled to avoid state mismatch
683+ if evictor , ok := nodeDownscaler ["evictor" ].(map [string ]interface {}); ok {
684+ if enabled , ok := evictor ["enabled" ].(bool ); ok && enabled {
685+ ev := flattenEvictor (evictor )
686+ if len (ev ) > 0 {
687+ nd [FieldEvictor ] = []interface {}{ev }
688+ }
689+ }
690+ }
691+
692+ return nd
693+ }
694+
695+ // flattenEvictor converts the evictor API response to Terraform structure.
696+ func flattenEvictor (evictor map [string ]interface {}) map [string ]interface {} {
697+ ev := make (map [string ]interface {})
698+
699+ if v , ok := evictor ["enabled" ]; ok {
700+ ev [FieldEnabled ] = v
701+ }
702+ if v , ok := evictor ["dryRun" ]; ok {
703+ ev [FieldEvictorDryRun ] = v
704+ }
705+ if v , ok := evictor ["aggressiveMode" ]; ok {
706+ ev [FieldEvictorAggressiveMode ] = v
707+ }
708+ if v , ok := evictor ["scopedMode" ]; ok {
709+ ev [FieldEvictorScopedMode ] = v
710+ }
711+ if v , ok := evictor ["cycleInterval" ]; ok {
712+ ev [FieldEvictorCycleInterval ] = v
713+ }
714+ if v , ok := evictor ["nodeGracePeriodMinutes" ]; ok {
715+ ev [FieldEvictorNodeGracePeriodMinutes ] = v
716+ }
717+ if v , ok := evictor ["podEvictionFailureBackOffInterval" ]; ok {
718+ ev [FieldEvictorPodEvictionFailureBackOffInterval ] = v
719+ }
720+ if v , ok := evictor ["ignorePodDisruptionBudgets" ]; ok {
721+ ev [FieldEvictorIgnorePodDisruptionBudgets ] = v
722+ }
723+
724+ return ev
725+ }
726+
516727func resourceCastaiAutoscalerDiff (ctx context.Context , d * schema.ResourceDiff , meta interface {}) error {
517728 clusterId := getClusterId (d )
518729 if clusterId == "" {
@@ -749,11 +960,42 @@ func createValidationError(field, value string) error {
749960// and their values are now stored in pre-existing states, so we cannot use the policies stored in state as-is. We null
750961// those fields in case they are not specified in the configuration.
751962func adjustPolicyForDrift (data types.ResourceProvider , policy * types.AutoscalerPolicy ) {
752- if policy != nil && policy .UnschedulablePods != nil {
753- val , d := data .GetRawConfigAt (cty .GetAttrPath (FieldAutoscalerSettings ).IndexInt (0 ).GetAttr (FieldUnschedulablePods ).IndexInt (0 ).GetAttr (FieldCustomInstancesEnabled ))
963+ if policy == nil {
964+ return
965+ }
966+
967+ // Handle deprecated spot_instances field
968+ val , d := data .GetRawConfigAt (cty .GetAttrPath (FieldAutoscalerSettings ).IndexInt (0 ).GetAttr (FieldSpotInstances ))
969+ if ! d .HasError () && val .IsNull () {
970+ policy .SpotInstances = nil
971+ }
972+
973+ if policy .UnschedulablePods != nil {
974+ unschedulablePodsPath := cty .GetAttrPath (FieldAutoscalerSettings ).IndexInt (0 ).GetAttr (FieldUnschedulablePods ).IndexInt (0 )
975+
976+ // Handle custom_instances_enabled (deprecated)
977+ val , d := data .GetRawConfigAt (unschedulablePodsPath .GetAttr (FieldCustomInstancesEnabled ))
754978 if ! d .HasError () && val .IsNull () {
755979 policy .UnschedulablePods .CustomInstances = nil
756980 }
981+
982+ // Handle headroom (deprecated)
983+ val , d = data .GetRawConfigAt (unschedulablePodsPath .GetAttr (FieldHeadroom ))
984+ if ! d .HasError () && val .IsNull () {
985+ policy .UnschedulablePods .Headroom = nil
986+ }
987+
988+ // Handle headroom_spot (deprecated)
989+ val , d = data .GetRawConfigAt (unschedulablePodsPath .GetAttr (FieldHeadroomSpot ))
990+ if ! d .HasError () && val .IsNull () {
991+ policy .UnschedulablePods .HeadroomSpot = nil
992+ }
993+
994+ // Handle node_constraints (deprecated)
995+ val , d = data .GetRawConfigAt (unschedulablePodsPath .GetAttr (FieldNodeConstraints ))
996+ if ! d .HasError () && val .IsNull () {
997+ policy .UnschedulablePods .NodeConstraints = nil
998+ }
757999 }
7581000}
7591001
0 commit comments