Skip to content

Commit 2733711

Browse files
author
Furkhat Kasymov Genii Uulu
committed
Merge branch 'master' of github.com:castai/terraform-provider-castai into federation-id-aks
2 parents 6d3dbe7 + 5188973 commit 2733711

14 files changed

+993
-90
lines changed

castai/provider_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ func testAccPreCheck(t *testing.T) {
8888
})
8989
}
9090

91-
// concatenateConfigs can be called to concatenate multiple strings to build test configurations
92-
func concatenateConfigs(config ...string) string {
91+
// ConfigCompose can be called to concatenate multiple strings to build test configurations
92+
func ConfigCompose(config ...string) string {
9393
var str strings.Builder
9494
for _, conf := range config {
9595
str.WriteString(conf)

castai/resource_allocation_group_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func allocationGroupWithAllClusterIds() string {
9393
cluster_ids = []
9494
}
9595
`
96-
return concatenateConfigs(cfg)
96+
return ConfigCompose(cfg)
9797
}
9898

9999
func allocationGroupConfig() string {
@@ -116,7 +116,7 @@ func allocationGroupConfig() string {
116116
}
117117
`
118118

119-
return concatenateConfigs(cfg)
119+
return ConfigCompose(cfg)
120120
}
121121

122122
func allocationGroupUpdatedConfig() string {
@@ -140,5 +140,5 @@ func allocationGroupUpdatedConfig() string {
140140
}
141141
`
142142

143-
return concatenateConfigs(cfg)
143+
return ConfigCompose(cfg)
144144
}

castai/resource_autoscaler.go

Lines changed: 286 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,210 @@ 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+
// Filter out volatile fields that change independently on API side
542+
currentPolicies = filterVolatileFields(currentPolicies)
543+
544+
// Set the computed autoscaler_policies field
545+
if err := d.Set(FieldAutoscalerPolicies, string(currentPolicies)); err != nil {
546+
return nil, fmt.Errorf("setting autoscaler_policies: %w", err)
547+
}
548+
549+
// Convert API response to autoscaler_settings structure
550+
settings, err := flattenAutoscalerSettings(currentPolicies)
551+
if err != nil {
552+
return nil, fmt.Errorf("converting policies to autoscaler_settings: %w", err)
553+
}
554+
555+
if settings != nil {
556+
if err := d.Set(FieldAutoscalerSettings, settings); err != nil {
557+
return nil, fmt.Errorf("setting autoscaler_settings: %w", err)
558+
}
559+
}
560+
561+
return []*schema.ResourceData{d}, nil
562+
}
563+
564+
// flattenAutoscalerSettings converts API JSON response to autoscaler_settings Terraform structure.
565+
// It omits deprecated fields to encourage users to migrate to castai_node_template.
566+
func flattenAutoscalerSettings(policiesJSON []byte) ([]map[string]interface{}, error) {
567+
var apiPolicy map[string]interface{}
568+
if err := json.Unmarshal(policiesJSON, &apiPolicy); err != nil {
569+
return nil, fmt.Errorf("unmarshaling policies JSON: %w", err)
570+
}
571+
572+
settings := make(map[string]interface{})
573+
574+
// Top-level fields
575+
if v, ok := apiPolicy["enabled"]; ok {
576+
settings[FieldEnabled] = v
577+
}
578+
if v, ok := apiPolicy["isScopedMode"]; ok {
579+
settings[FieldIsScopedMode] = v
580+
}
581+
if v, ok := apiPolicy["nodeTemplatesPartialMatchingEnabled"]; ok {
582+
settings[FieldNodeTemplatesPartialMatchingEnabled] = v
583+
}
584+
585+
// Unschedulable Pods (excluding deprecated fields: headroom, headroomSpot, nodeConstraints, customInstancesEnabled)
586+
if unschedulablePods, ok := apiPolicy["unschedulablePods"].(map[string]interface{}); ok {
587+
up := flattenUnschedulablePods(unschedulablePods)
588+
if len(up) > 0 {
589+
settings[FieldUnschedulablePods] = []interface{}{up}
590+
}
591+
}
592+
593+
// Cluster Limits
594+
if clusterLimits, ok := apiPolicy["clusterLimits"].(map[string]interface{}); ok {
595+
cl := flattenClusterLimits(clusterLimits)
596+
if len(cl) > 0 {
597+
settings[FieldClusterLimits] = []interface{}{cl}
598+
}
599+
}
600+
601+
// Node Downscaler (excluding spot_instances which is deprecated at top level)
602+
if nodeDownscaler, ok := apiPolicy["nodeDownscaler"].(map[string]interface{}); ok {
603+
nd := flattenNodeDownscaler(nodeDownscaler)
604+
if len(nd) > 0 {
605+
settings[FieldNodeDownscaler] = []interface{}{nd}
606+
}
607+
}
608+
609+
// Note: Omitting spotInstances entirely (deprecated)
610+
611+
return []map[string]interface{}{settings}, nil
612+
}
613+
614+
// flattenUnschedulablePods converts the unschedulablePods API response to Terraform structure.
615+
// Deprecated fields (headroom, headroomSpot, nodeConstraints, customInstancesEnabled) are omitted.
616+
// podPinner is included if present in API response so users can track changes made via UI/API.
617+
func flattenUnschedulablePods(unschedulablePods map[string]interface{}) map[string]interface{} {
618+
up := make(map[string]interface{})
619+
620+
if v, ok := unschedulablePods["enabled"]; ok {
621+
up[FieldEnabled] = v
622+
}
623+
624+
// Note: Omitting headroom, headroomSpot, nodeConstraints, customInstancesEnabled (deprecated)
625+
626+
if podPinner, ok := unschedulablePods["podPinner"].(map[string]interface{}); ok {
627+
if enabled, ok := podPinner["enabled"].(bool); ok {
628+
pp := map[string]interface{}{
629+
FieldEnabled: enabled,
630+
}
631+
up[FieldPodPinner] = []interface{}{pp}
632+
}
633+
}
634+
635+
return up
636+
}
637+
638+
// flattenClusterLimits converts the clusterLimits API response to Terraform structure.
639+
func flattenClusterLimits(clusterLimits map[string]interface{}) map[string]interface{} {
640+
cl := make(map[string]interface{})
641+
642+
if v, ok := clusterLimits["enabled"]; ok {
643+
cl[FieldEnabled] = v
644+
}
645+
646+
if cpu, ok := clusterLimits["cpu"].(map[string]interface{}); ok {
647+
cpuMap := make(map[string]interface{})
648+
if v, ok := cpu["minCores"]; ok {
649+
cpuMap[FieldMinCores] = v
650+
}
651+
if v, ok := cpu["maxCores"]; ok {
652+
cpuMap[FieldMaxCores] = v
653+
}
654+
if len(cpuMap) > 0 {
655+
cl[FieldCPU] = []interface{}{cpuMap}
656+
}
657+
}
658+
659+
return cl
660+
}
661+
662+
// flattenNodeDownscaler converts the nodeDownscaler API response to Terraform structure.
663+
func flattenNodeDownscaler(nodeDownscaler map[string]interface{}) map[string]interface{} {
664+
nd := make(map[string]interface{})
665+
666+
if v, ok := nodeDownscaler["enabled"]; ok {
667+
nd[FieldEnabled] = v
668+
}
669+
670+
if emptyNodes, ok := nodeDownscaler["emptyNodes"].(map[string]interface{}); ok {
671+
en := make(map[string]interface{})
672+
if v, ok := emptyNodes["enabled"]; ok {
673+
en[FieldEnabled] = v
674+
}
675+
if v, ok := emptyNodes["delaySeconds"]; ok {
676+
en[FieldDelaySeconds] = v
677+
}
678+
if len(en) > 0 {
679+
nd[FieldEmptyNodes] = []interface{}{en}
680+
}
681+
}
682+
683+
if evictor, ok := nodeDownscaler["evictor"].(map[string]interface{}); ok {
684+
ev := flattenEvictor(evictor)
685+
if len(ev) > 0 {
686+
nd[FieldEvictor] = []interface{}{ev}
687+
}
688+
}
689+
690+
return nd
691+
}
692+
693+
// flattenEvictor converts the evictor API response to Terraform structure.
694+
func flattenEvictor(evictor map[string]interface{}) map[string]interface{} {
695+
ev := make(map[string]interface{})
696+
697+
if v, ok := evictor["enabled"]; ok {
698+
ev[FieldEnabled] = v
699+
}
700+
if v, ok := evictor["dryRun"]; ok {
701+
ev[FieldEvictorDryRun] = v
702+
}
703+
if v, ok := evictor["aggressiveMode"]; ok {
704+
ev[FieldEvictorAggressiveMode] = v
705+
}
706+
if v, ok := evictor["scopedMode"]; ok {
707+
ev[FieldEvictorScopedMode] = v
708+
}
709+
if v, ok := evictor["cycleInterval"]; ok {
710+
ev[FieldEvictorCycleInterval] = v
711+
}
712+
if v, ok := evictor["nodeGracePeriodMinutes"]; ok {
713+
ev[FieldEvictorNodeGracePeriodMinutes] = v
714+
}
715+
if v, ok := evictor["podEvictionFailureBackOffInterval"]; ok {
716+
ev[FieldEvictorPodEvictionFailureBackOffInterval] = v
717+
}
718+
if v, ok := evictor["ignorePodDisruptionBudgets"]; ok {
719+
ev[FieldEvictorIgnorePodDisruptionBudgets] = v
720+
}
721+
722+
return ev
723+
}
724+
516725
func resourceCastaiAutoscalerDiff(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error {
517726
clusterId := getClusterId(d)
518727
if clusterId == "" {
@@ -527,6 +736,9 @@ func resourceCastaiAutoscalerDiff(ctx context.Context, d *schema.ResourceDiff, m
527736
return nil
528737
}
529738

739+
// Filter out volatile fields that change independently on API side
740+
policies = filterVolatileFields(policies)
741+
530742
return d.SetNew(FieldAutoscalerPolicies, string(policies))
531743
}
532744

@@ -639,6 +851,9 @@ func readAutoscalerPolicies(ctx context.Context, data *schema.ResourceData, meta
639851
return err
640852
}
641853

854+
// Filter out volatile fields that change independently on API side
855+
currentPolicies = filterVolatileFields(currentPolicies)
856+
642857
err = data.Set(FieldAutoscalerPolicies, string(currentPolicies))
643858
if err != nil {
644859
log.Printf("[ERROR] Failed to set field: %v", err)
@@ -657,6 +872,43 @@ func getClusterId(data types.ResourceProvider) string {
657872
return value.(string)
658873
}
659874

875+
// filterVolatileFields removes API-computed fields that change independently and cause unnecessary drift.
876+
// This includes fields that are bidirectionally synced with node templates.
877+
func filterVolatileFields(policiesJSON []byte) []byte {
878+
var policies map[string]interface{}
879+
if err := json.Unmarshal(policiesJSON, &policies); err != nil {
880+
return policiesJSON // return as-is if unmarshal fails
881+
}
882+
883+
// Remove fields that change due to policy↔node template sync
884+
delete(policies, "defaultNodeTemplateVersion")
885+
delete(policies, "spotInstances") // synced with node template spot settings
886+
887+
// Remove nested deprecated fields that sync with node templates
888+
if up, ok := policies["unschedulablePods"].(map[string]interface{}); ok {
889+
delete(up, "nodeConstraints") // synced with node template min/max CPU/RAM
890+
delete(up, "customInstancesEnabled") // synced with node template
891+
892+
// Remove API-computed status fields that change based on cluster state
893+
if podPinner, ok := up["podPinner"].(map[string]interface{}); ok {
894+
delete(podPinner, "status")
895+
}
896+
}
897+
898+
// Remove API-computed status field from evictor
899+
if nd, ok := policies["nodeDownscaler"].(map[string]interface{}); ok {
900+
if evictor, ok := nd["evictor"].(map[string]interface{}); ok {
901+
delete(evictor, "status")
902+
}
903+
}
904+
905+
filtered, err := json.Marshal(policies)
906+
if err != nil {
907+
return policiesJSON
908+
}
909+
return filtered
910+
}
911+
660912
func getChangedPolicies(ctx context.Context, data types.ResourceProvider, meta interface{}, clusterId string) ([]byte, error) {
661913
policyChangesJSON, isPoliciesJSONExist := data.GetOk(FieldAutoscalerPoliciesJSON)
662914
_, isPoliciesSettingsExist := data.GetOk(FieldAutoscalerSettings)
@@ -749,11 +1001,42 @@ func createValidationError(field, value string) error {
7491001
// and their values are now stored in pre-existing states, so we cannot use the policies stored in state as-is. We null
7501002
// those fields in case they are not specified in the configuration.
7511003
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))
1004+
if policy == nil {
1005+
return
1006+
}
1007+
1008+
// Handle deprecated spot_instances field
1009+
val, d := data.GetRawConfigAt(cty.GetAttrPath(FieldAutoscalerSettings).IndexInt(0).GetAttr(FieldSpotInstances))
1010+
if !d.HasError() && val.IsNull() {
1011+
policy.SpotInstances = nil
1012+
}
1013+
1014+
if policy.UnschedulablePods != nil {
1015+
unschedulablePodsPath := cty.GetAttrPath(FieldAutoscalerSettings).IndexInt(0).GetAttr(FieldUnschedulablePods).IndexInt(0)
1016+
1017+
// Handle custom_instances_enabled (deprecated)
1018+
val, d := data.GetRawConfigAt(unschedulablePodsPath.GetAttr(FieldCustomInstancesEnabled))
7541019
if !d.HasError() && val.IsNull() {
7551020
policy.UnschedulablePods.CustomInstances = nil
7561021
}
1022+
1023+
// Handle headroom (deprecated)
1024+
val, d = data.GetRawConfigAt(unschedulablePodsPath.GetAttr(FieldHeadroom))
1025+
if !d.HasError() && val.IsNull() {
1026+
policy.UnschedulablePods.Headroom = nil
1027+
}
1028+
1029+
// Handle headroom_spot (deprecated)
1030+
val, d = data.GetRawConfigAt(unschedulablePodsPath.GetAttr(FieldHeadroomSpot))
1031+
if !d.HasError() && val.IsNull() {
1032+
policy.UnschedulablePods.HeadroomSpot = nil
1033+
}
1034+
1035+
// Handle node_constraints (deprecated)
1036+
val, d = data.GetRawConfigAt(unschedulablePodsPath.GetAttr(FieldNodeConstraints))
1037+
if !d.HasError() && val.IsNull() {
1038+
policy.UnschedulablePods.NodeConstraints = nil
1039+
}
7571040
}
7581041
}
7591042

0 commit comments

Comments
 (0)