Skip to content

Commit f75a436

Browse files
authored
fix: handle custom instances field policy drift (#577)
1 parent 2f1b7eb commit f75a436

6 files changed

Lines changed: 169 additions & 20 deletions

File tree

castai/resource_autoscaler.go

Lines changed: 17 additions & 0 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/hashicorp/go-cty/cty"
1415
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1516
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1617
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -668,6 +669,8 @@ func getChangedPolicies(ctx context.Context, data types.ResourceProvider, meta i
668669
return nil, fmt.Errorf("failed to deserialize policy definitions: %v", err)
669670
}
670671

672+
adjustPolicyForDrift(data, policy)
673+
671674
data, err := json.Marshal(policy)
672675
if err != nil {
673676
return nil, fmt.Errorf("failed to serialize policy definition: %v", err)
@@ -740,6 +743,20 @@ func createValidationError(field, value string) error {
740743
"Policy:\n%v", field, value)
741744
}
742745

746+
// adjustPolicyForDrift removes values of certain fields which, if retained, might result in constant plan changes.
747+
// Changes to legacy autoscaler policies and default node template sync to one another, and if values of certain fields
748+
// are not equal, then they'll keep producing plan changes. Originally, some of these fields had default values
749+
// and their values are now stored in pre-existing states, so we cannot use the policies stored in state as-is. We null
750+
// those fields in case they are not specified in the configuration.
751+
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))
754+
if !d.HasError() && val.IsNull() {
755+
policy.UnschedulablePods.CustomInstances = nil
756+
}
757+
}
758+
}
759+
743760
// toAutoscalerPolicy converts FieldAutoscalerSettings to types.AutoscalerPolicy for given data.
744761
func toAutoscalerPolicy(data types.ResourceProvider) (*types.AutoscalerPolicy, error) {
745762
out, ok := extractNestedValues(data, FieldAutoscalerSettings, true, true)

castai/resource_autoscaler_test.go

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/golang/mock/gomock"
1515
"github.com/hashicorp/go-cty/cty"
1616
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
17+
"github.com/samber/lo"
1718
"github.com/stretchr/testify/require"
1819

1920
"github.com/castai/terraform-provider-castai/castai/sdk"
@@ -592,20 +593,17 @@ func TestAutoscalerResource_CustomizeDiff(t *testing.T) {
592593
}
593594

594595
func TestAutoscalerResource_ToAutoscalerPolicy(t *testing.T) {
595-
tt := []struct {
596-
name string
596+
tt := map[string]struct {
597597
data cty.Value
598598
expected *types.AutoscalerPolicy
599599
shouldFail bool
600600
expectedErr error
601601
}{
602-
{
603-
name: "should return nil when data is nil",
602+
"should return nil when data is nil": {
604603
data: cty.NilVal,
605604
expected: nil,
606605
},
607-
{
608-
name: "should handle nested objects",
606+
"should handle nested objects": {
609607
data: cty.ObjectVal(
610608
map[string]cty.Value{
611609
FieldAutoscalerSettings: cty.ListVal(
@@ -640,7 +638,8 @@ func TestAutoscalerResource_ToAutoscalerPolicy(t *testing.T) {
640638
expected: &types.AutoscalerPolicy{
641639
Enabled: true,
642640
UnschedulablePods: &types.UnschedulablePods{
643-
Enabled: true,
641+
Enabled: true,
642+
CustomInstances: lo.ToPtr(false),
644643
PodPinner: &types.PodPinner{
645644
Enabled: true,
646645
},
@@ -649,10 +648,10 @@ func TestAutoscalerResource_ToAutoscalerPolicy(t *testing.T) {
649648
},
650649
}
651650

652-
for _, test := range tt {
651+
for testName, test := range tt {
653652
r := require.New(t)
654653

655-
t.Run(test.name, func(t *testing.T) {
654+
t.Run(testName, func(t *testing.T) {
656655
resource := resourceAutoscaler()
657656
state := terraform.NewInstanceStateShimmedFromValue(test.data, 0)
658657
actual, err := toAutoscalerPolicy(resource.Data(state))
@@ -857,6 +856,108 @@ func TestAutoscalerResource_GetChangePolicies_ComparePolicyJsonAndDef(t *testing
857856
}
858857
}
859858

859+
// Checks if the value of custom_instances_enabled is retained as received from the API
860+
// in case when it is not specified (null) in the resource configuration
861+
func TestAutoscalerResource_GetChangePolicies_AdjustPolicyForDrift(t *testing.T) {
862+
r := require.New(t)
863+
mockCtrl := gomock.NewController(t)
864+
mockClient := mock_sdk.NewMockClientInterface(mockCtrl)
865+
clusterID := "cluster_id"
866+
provider := &ProviderConfig{
867+
api: &sdk.ClientWithResponses{
868+
ClientInterface: mockClient,
869+
},
870+
}
871+
originalPolicyStr := `{
872+
"enabled": true,
873+
"isScopedMode": false,
874+
"nodeTemplatesPartialMatchingEnabled": false,
875+
"unschedulablePods": {
876+
"enabled": true,
877+
"custom_instances_enabled": true
878+
}
879+
}`
880+
expectedPolicyStr := `{
881+
"enabled": false,
882+
"isScopedMode": false,
883+
"nodeTemplatesPartialMatchingEnabled": false,
884+
"unschedulablePods": {
885+
"enabled": false,
886+
"custom_instances_enabled": true
887+
}
888+
}`
889+
890+
policyJSON, err := normalizeJSON([]byte(originalPolicyStr))
891+
r.NoError(err)
892+
893+
expectedPolicyJSON, err := normalizeJSON([]byte(expectedPolicyStr))
894+
r.NoError(err)
895+
896+
mockClient.EXPECT().
897+
PoliciesAPIGetClusterPolicies(gomock.Any(), gomock.Any(), gomock.Any()).
898+
Times(1).
899+
DoAndReturn(
900+
func(_ context.Context, _ string) (*http.Response, error) {
901+
return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(policyJSON))}, nil
902+
},
903+
)
904+
905+
value := cty.ObjectVal(map[string]cty.Value{
906+
FieldClusterId: cty.StringVal(clusterID),
907+
FieldAutoscalerSettings: cty.ListVal(
908+
[]cty.Value{
909+
cty.ObjectVal(
910+
map[string]cty.Value{
911+
"enabled": cty.BoolVal(false),
912+
"unschedulable_pods": cty.ListVal(
913+
[]cty.Value{
914+
cty.ObjectVal(
915+
map[string]cty.Value{
916+
"enabled": cty.BoolVal(false),
917+
},
918+
),
919+
},
920+
),
921+
},
922+
),
923+
},
924+
),
925+
})
926+
rawConfig := cty.ObjectVal(
927+
map[string]cty.Value{
928+
"autoscaler_settings": cty.ListVal(
929+
[]cty.Value{
930+
cty.ObjectVal(
931+
map[string]cty.Value{
932+
"enabled": cty.BoolVal(false),
933+
"unschedulable_pods": cty.ListVal(
934+
[]cty.Value{
935+
cty.ObjectVal(
936+
map[string]cty.Value{
937+
"enabled": cty.BoolVal(false),
938+
"custom_instances_enabled": cty.NullVal(cty.Bool),
939+
},
940+
),
941+
},
942+
),
943+
},
944+
),
945+
},
946+
),
947+
},
948+
)
949+
950+
stateWithPolicyDefinition := terraform.NewInstanceStateShimmedFromValue(value, 0)
951+
stateWithPolicyDefinition.RawConfig = rawConfig
952+
953+
resource := resourceAutoscaler()
954+
data := resource.Data(stateWithPolicyDefinition)
955+
result, err := getChangedPolicies(context.Background(), data, provider, clusterID)
956+
957+
r.NoError(err)
958+
r.Equal(string(expectedPolicyJSON), string(result))
959+
}
960+
860961
func JSONBytesEqual(a, b []byte) (bool, error) {
861962
var j, j2 interface{}
862963
if err := json.Unmarshal(a, &j); err != nil {

castai/sdk/api.gen.go

Lines changed: 18 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

castai/types/autoscaler_policies.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type UnschedulablePods struct {
1515
Headroom *Headroom `mapstructure:"headroom" json:"headroom,omitempty"`
1616
HeadroomSpot *Headroom `mapstructure:"headroom_spot" json:"headroomSpot,omitempty"`
1717
NodeConstraints *NodeConstraints `mapstructure:"node_constraints" json:"nodeConstraints,omitempty"`
18-
CustomInstances bool `mapstructure:"custom_instances_enabled" json:"customInstancesEnabled"`
18+
CustomInstances *bool `mapstructure:"custom_instances_enabled" json:"customInstancesEnabled,omitempty"`
1919
PodPinner *PodPinner `mapstructure:"pod_pinner" json:"podPinner,omitempty"`
2020
}
2121

castai/types/resource.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package types
22

3+
import (
4+
"github.com/hashicorp/go-cty/cty"
5+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
6+
)
7+
38
// ResourceProvider defines an common interface for *schema.ResourceData and *schema.ResourceDiff
49
type ResourceProvider interface {
510
// GetOk functions the same way as ResourceData.GetOk, but it also checks the
@@ -15,4 +20,9 @@ type ResourceProvider interface {
1520
//
1621
// for more info: *schema.ResourceData or *schema.ResourceDiff
1722
GetOkExists(key string) (interface{}, bool)
23+
24+
// GetRawConfigAt returns a value from the raw config at the given path. In some cases this is necessary
25+
// to determine whether an optional field has been defined when accounting for potential state drift due to
26+
// external changes.
27+
GetRawConfigAt(valPath cty.Path) (cty.Value, diag.Diagnostics)
1828
}

main.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"flag"
5+
46
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
57
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
68

@@ -13,9 +15,18 @@ var (
1315
)
1416

1517
func main() {
16-
plugin.Serve(&plugin.ServeOpts{
18+
var debug bool
19+
20+
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
21+
flag.Parse()
22+
23+
opts := &plugin.ServeOpts{
24+
Debug: debug,
25+
ProviderAddr: "registry.terraform.io/castai/castai",
1726
ProviderFunc: func() *schema.Provider {
1827
return castai.Provider(version)
1928
},
20-
})
29+
}
30+
31+
plugin.Serve(opts)
2132
}

0 commit comments

Comments
 (0)