Skip to content

Commit 4701b13

Browse files
committed
feat: custom importer for autoscaler resource
1 parent 0124b99 commit 4701b13

File tree

3 files changed

+505
-4
lines changed

3 files changed

+505
-4
lines changed

castai/resource_autoscaler.go

Lines changed: 245 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
516727
func 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.
751962
func 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

Comments
 (0)