Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ jobs:
SSO_CLIENT_ID: ${{ secrets.SSO_CLIENT_ID }}
SSO_CLIENT_SECRET: ${{ secrets.SSO_CLIENT_SECRET }}
SSO_DOMAIN: ${{ secrets.SSO_DOMAIN }}
ACCEPTANCE_TEST_ORGANIZATION_ID: ${{ vars.TF_ACCEPTANCE_TEST_ORGANIZATION_ID }}
run: make testacc

3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ SHELL := /bin/bash
export API_TAGS ?= ExternalClusterAPI,PoliciesAPI,NodeConfigurationAPI,NodeTemplatesAPI,AuthTokenAPI,ScheduledRebalancingAPI,InventoryAPI,UsersAPI,OperationsAPI,EvictorAPI,SSOAPI,CommitmentsAPI,WorkloadOptimizationAPI,ServiceAccountsAPI,RbacServiceAPI
export SWAGGER_LOCATION ?= https://api.cast.ai/v1/spec/openapi.json

export CLUSTER_AUTOSCALER_API_TAGS ?= HibernationSchedulesAPI
export CLUSTER_AUTOSCALER_SWAGGER_LOCATION ?= https://api.cast.ai/spec/cluster-autoscaler/openapi.yaml

default: build

.PHONY: init-examples
Expand Down
49 changes: 49 additions & 0 deletions castai/data_source_resource_hibernation_schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package castai

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceHibernationSchedule() *schema.Resource {
dataSourceHibernationSchedule := &schema.Resource{
Description: "Retrieve Hibernation Schedule ",
ReadContext: dataSourceHibernationScheduleRead,
Schema: map[string]*schema.Schema{},
}

resourceHibernationSchedule := resourceHibernationSchedule()
for key, value := range resourceHibernationSchedule.Schema {
dataSourceHibernationSchedule.Schema[key] = value
if key != FieldHibernationScheduleName && key != FieldHibernationScheduleOrganizationID {
// only name and optionally organization id are provided in terraform configuration by user
// other parameters are "computed" from existing hibernation schedule
dataSourceHibernationSchedule.Schema[key].Computed = true
dataSourceHibernationSchedule.Schema[key].Required = false
// MaxItems is for configurable attributes, there's nothing to configure on computed-only field
dataSourceHibernationSchedule.Schema[key].MaxItems = 0
}
}
return dataSourceHibernationSchedule
}

func dataSourceHibernationScheduleRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
organizationID, err := getHibernationScheduleOrganizationID(ctx, data, meta)
if err != nil {
return diag.FromErr(fmt.Errorf("error retrieving hibernation schedule organization id: %w", err))
}

scheduleName := data.Get(FieldHibernationScheduleName).(string)
schedule, err := getHibernationScheduleByName(ctx, meta, organizationID, scheduleName)
if err != nil {
return diag.FromErr(fmt.Errorf("error retrieving hibernation schedule: %w", err))
}

if err := hibernationScheduleToState(schedule, data); err != nil {
return diag.FromErr(fmt.Errorf("error converting schdeure to terraform state: %w", err))
}
return nil
}
136 changes: 136 additions & 0 deletions castai/data_source_resource_hibernation_schedule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package castai

import (
"bytes"
"context"
"io"
"net/http"
"testing"

"github.com/golang/mock/gomock"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/samber/lo"
"github.com/stretchr/testify/require"

"github.com/castai/terraform-provider-castai/castai/sdk"
"github.com/castai/terraform-provider-castai/castai/sdk/cluster_autoscaler"
mock_cluster_autoscaler "github.com/castai/terraform-provider-castai/castai/sdk/cluster_autoscaler/mock"
mock_sdk "github.com/castai/terraform-provider-castai/castai/sdk/mock"
)

func TestHibernationScheduleDataSourceRead(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()

r := require.New(t)
mockClient := mock_sdk.NewMockClientWithResponsesInterface(ctrl)
clusterAutoscalerClient := mock_cluster_autoscaler.NewMockClientInterface(ctrl)

ctx := context.Background()
provider := &ProviderConfig{
api: mockClient,
clusterAutoscalerClient: &cluster_autoscaler.ClientWithResponses{
ClientInterface: clusterAutoscalerClient,
},
}

organizationID := "0d0111f9-e5a4-4acc-85b2-4c3a8318dfc2"
body := io.NopCloser(bytes.NewReader([]byte(`{
"items": [
{
"id": "75c04e4e-f95c-4f24-a814-b8e753a5194d",
"organizationId": "0d0111f9-e5a4-4acc-85b2-4c3a8318dfc2",
"enabled": false,
"name": "schedule",
"pauseConfig": {
"enabled": true,
"schedule": {
"cronExpression": "1 0 * * *"
}
},
"resumeConfig": {
"enabled": true,
"schedule": {
"cronExpression": "1 0 * * *"
},
"jobConfig": {
"nodeConfig": {
"instanceType": "e2-standard-4",
"kubernetesLabels": {},
"kubernetesTaints": []
}
}
},
"clusterAssignments": {
"items": [
{
"clusterId": "38a49ce8-e900-4a10-be89-48fb2efb1025"
}
]
},
"createTime": "2025-04-10T12:52:07.732194Z",
"updateTime": "2025-04-10T12:52:07.732194Z"
}
],
"nextPageCursor": "",
"totalCount": 1
}`)))

mockClient.EXPECT().UsersAPIListOrganizationsWithResponse(gomock.Any()).Return(&sdk.UsersAPIListOrganizationsResponse{
JSON200: &sdk.CastaiUsersV1beta1ListOrganizationsResponse{
Organizations: []sdk.CastaiUsersV1beta1UserOrganization{
{Id: lo.ToPtr(organizationID)},
},
},
HTTPResponse: &http.Response{StatusCode: http.StatusOK},
}, nil).Times(1)

clusterAutoscalerClient.EXPECT().
HibernationSchedulesAPIListHibernationSchedules(gomock.Any(), organizationID, gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil)

state := terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{}), 0)

resource := dataSourceHibernationSchedule()
data := resource.Data(state)

r.NoError(data.Set("name", "schedule"))

result := resource.ReadContext(ctx, data, provider)
r.Nil(result)
r.False(result.HasError())

expectedState := `ID = 75c04e4e-f95c-4f24-a814-b8e753a5194d
cluster_assignments.# = 1
cluster_assignments.0.assignment.# = 1
cluster_assignments.0.assignment.0.cluster_id = 38a49ce8-e900-4a10-be89-48fb2efb1025
enabled = false
name = schedule
organization_id = 0d0111f9-e5a4-4acc-85b2-4c3a8318dfc2
pause_config.# = 1
pause_config.0.enabled = true
pause_config.0.schedule.# = 1
pause_config.0.schedule.0.cron_expression = 1 0 * * *
resume_config.# = 1
resume_config.0.enabled = true
resume_config.0.job_config.# = 1
resume_config.0.job_config.0.node_config.# = 1
resume_config.0.job_config.0.node_config.0.config_id =
resume_config.0.job_config.0.node_config.0.config_name =
resume_config.0.job_config.0.node_config.0.gpu_config.# = 0
resume_config.0.job_config.0.node_config.0.instance_type = e2-standard-4
resume_config.0.job_config.0.node_config.0.kubernetes_labels.% = 0
resume_config.0.job_config.0.node_config.0.kubernetes_taints.# = 0
resume_config.0.job_config.0.node_config.0.node_affinity.# = 0
resume_config.0.job_config.0.node_config.0.spot_config.# = 0
resume_config.0.job_config.0.node_config.0.subnet_id =
resume_config.0.job_config.0.node_config.0.volume.# = 0
resume_config.0.job_config.0.node_config.0.zone =
resume_config.0.schedule.# = 1
resume_config.0.schedule.0.cron_expression = 1 0 * * *
Tainted = false
`
r.Equal(expectedState, data.State().String())
}
16 changes: 14 additions & 2 deletions castai/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

"github.com/castai/terraform-provider-castai/castai/sdk"
"github.com/castai/terraform-provider-castai/castai/sdk/cluster_autoscaler"
)

type ProviderConfig struct {
api sdk.ClientWithResponsesInterface
api sdk.ClientWithResponsesInterface
clusterAutoscalerClient cluster_autoscaler.ClientWithResponsesInterface
}

func Provider(version string) *schema.Provider {
Expand Down Expand Up @@ -57,13 +59,15 @@ func Provider(version string) *schema.Provider {
"castai_workload_scaling_policy": resourceWorkloadScalingPolicy(),
"castai_organization_group": resourceOrganizationGroup(),
"castai_role_bindings": resourceRoleBindings(),
"castai_hibernation_schedule": resourceHibernationSchedule(),
},

DataSourcesMap: map[string]*schema.Resource{
"castai_eks_settings": dataSourceEKSSettings(),
"castai_gke_user_policies": dataSourceGKEPolicies(),
"castai_organization": dataSourceOrganization(),
"castai_rebalancing_schedule": dataSourceRebalancingSchedule(),
"castai_hibernation_schedule": dataSourceHibernationSchedule(),

// TODO: remove in next major release
"castai_eks_user_arn": dataSourceEKSClusterUserARN(),
Expand All @@ -90,6 +94,14 @@ func providerConfigure(version string) schema.ConfigureContextFunc {
return nil, diag.FromErr(err)
}

return &ProviderConfig{api: client}, nil
clusterAutoscalerClient, err := cluster_autoscaler.CreateClient(apiURL, apiToken, agent)
if err != nil {
return nil, diag.FromErr(err)
}

return &ProviderConfig{
api: client,
clusterAutoscalerClient: clusterAutoscalerClient,
}, nil
}
}
4 changes: 4 additions & 0 deletions castai/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func testAccPreCheck(t *testing.T) {
t.Fatal("CASTAI_API_TOKEN must be set for acceptance tests")
}

if v := os.Getenv("ACCEPTANCE_TEST_ORGANIZATION_ID"); v == "" {
t.Fatal("ACCEPTANCE_TEST_ORGANIZATION_ID must be set for acceptance tests")
}

if err := testAccProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(nil)); err != nil {
t.Fatal(err)
}
Expand Down
32 changes: 17 additions & 15 deletions castai/resource_eviction_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/castai/terraform-provider-castai/castai/sdk"
"log"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/samber/lo"
"log"
"time"

"github.com/castai/terraform-provider-castai/castai/sdk"
)

const (
Expand Down Expand Up @@ -124,18 +126,18 @@ func resourceEvictionConfig() *schema.Resource {
},
},
FieldEvictionOptionDisabled: {
Type: schema.TypeBool,
Optional: true,
Type: schema.TypeBool,
Optional: true,
Description: "Mark pods as removal disabled",
},
FieldEvictionOptionAggressive: {
Type: schema.TypeBool,
Optional: true,
Type: schema.TypeBool,
Optional: true,
Description: "Apply Aggressive mode to Evictor",
},
FieldEvictionOptionDisposable: {
Type: schema.TypeBool,
Optional: true,
Type: schema.TypeBool,
Optional: true,
Description: "Mark node as disposable",
},
},
Expand Down Expand Up @@ -424,7 +426,7 @@ func toPodSelector(in interface{}) (*sdk.CastaiEvictorV1PodSelector, error) {
return nil, err
}

if mls == nil || len(mls.AdditionalProperties) == 0 {
if mls == nil || len(*mls) == 0 {
continue
}

Expand Down Expand Up @@ -474,21 +476,21 @@ func toNodeSelector(in interface{}) (*sdk.CastaiEvictorV1NodeSelector, error) {
return &out, nil
}

func toMatchLabels(in interface{}) (*sdk.CastaiEvictorV1LabelSelector_MatchLabels, error) {
func toMatchLabels(in interface{}) (*map[string]string, error) {
mls, ok := in.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("mapping match_labels expecting map[string]interface, got %T %+v", in, in)
}
if len(mls) == 0 {
return nil, nil
}
out := sdk.CastaiEvictorV1LabelSelector_MatchLabels{AdditionalProperties: map[string]string{}}
out := map[string]string{}
for k, v := range mls {
value, ok := v.(string)
if !ok {
return nil, fmt.Errorf("mapping match_labels expecting string, got %T %+v", v, v)
}
out.AdditionalProperties[k] = value
out[k] = value
}

return &out, nil
Expand All @@ -507,7 +509,7 @@ func flattenPodSelector(ps *sdk.CastaiEvictorV1PodSelector) []map[string]any {
}
if ps.LabelSelector != nil {
if ps.LabelSelector.MatchLabels != nil {
out[FieldMatchLabels] = ps.LabelSelector.MatchLabels.AdditionalProperties
out[FieldMatchLabels] = *ps.LabelSelector.MatchLabels
}
if ps.LabelSelector.MatchExpressions != nil {
out[FieldMatchExpressions] = flattenMatchExpressions(*ps.LabelSelector.MatchExpressions)
Expand All @@ -522,7 +524,7 @@ func flattenNodeSelector(ns *sdk.CastaiEvictorV1NodeSelector) []map[string]any {
}
out := map[string]any{}
if ns.LabelSelector.MatchLabels != nil {
out[FieldMatchLabels] = ns.LabelSelector.MatchLabels.AdditionalProperties
out[FieldMatchLabels] = *ns.LabelSelector.MatchLabels
}
if ns.LabelSelector.MatchExpressions != nil {
out[FieldMatchExpressions] = flattenMatchExpressions(*ns.LabelSelector.MatchExpressions)
Expand Down
16 changes: 9 additions & 7 deletions castai/resource_eviction_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import (
"bytes"
"context"
"fmt"
"github.com/castai/terraform-provider-castai/castai/sdk"
mock_sdk "github.com/castai/terraform-provider-castai/castai/sdk/mock"
"io"
"net/http"
"testing"

"github.com/golang/mock/gomock"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
"io"
"net/http"
"testing"

"github.com/castai/terraform-provider-castai/castai/sdk"
mock_sdk "github.com/castai/terraform-provider-castai/castai/sdk/mock"
)

func TestEvictionConfig_ReadContext(t *testing.T) {
Expand Down Expand Up @@ -299,9 +301,9 @@ func TestEvictionConfig_UpdateContext(t *testing.T) {
PodSelector: &sdk.CastaiEvictorV1PodSelector{
Kind: lo.ToPtr("Job"),
LabelSelector: &sdk.CastaiEvictorV1LabelSelector{
MatchLabels: &sdk.CastaiEvictorV1LabelSelector_MatchLabels{AdditionalProperties: map[string]string{
MatchLabels: &map[string]string{
"key1": "value1",
}}}}}
}}}}

newConfig := sdk.CastaiEvictorV1EvictionConfig{
Settings: sdk.CastaiEvictorV1EvictionSettings{Disposable: &sdk.CastaiEvictorV1EvictionSettingsSettingEnabled{Enabled: true}},
Expand Down
Loading
Loading