diff --git a/README.md b/README.md index 5091f6f7..73cfda60 100644 --- a/README.md +++ b/README.md @@ -473,6 +473,16 @@ $ cd terraform-provider-castai $ make build ``` +After you build the provider, you have to set the `~/.terraformrc` configuration to let terraform know you want to use local provider: +```terraform +provider_installation { + dev_overrides { + "castai/castai" = "" + } + direct {} +} +``` + _`make build` builds the provider and install symlinks to that build for all terraform projects in `examples/*` dir. Now you can work on `examples/localdev`._ diff --git a/castai/resource_sso_connection.go b/castai/resource_sso_connection.go index 30f9fece..d713b0b4 100644 --- a/castai/resource_sso_connection.go +++ b/castai/resource_sso_connection.go @@ -7,16 +7,18 @@ import ( "fmt" "time" - "github.com/castai/terraform-provider-castai/castai/sdk" "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" "golang.org/x/crypto/bcrypt" + + "github.com/castai/terraform-provider-castai/castai/sdk" ) const ( - FieldSSOConnectionName = "name" - FieldSSOConnectionEmailDomain = "email_domain" + FieldSSOConnectionName = "name" + FieldSSOConnectionEmailDomain = "email_domain" + FieldSSOConnectionAdditionalEmailDomains = "additional_email_domains" FieldSSOConnectionAAD = "aad" FieldSSOConnectionADDomain = "ad_domain" @@ -55,6 +57,17 @@ func resourceSSOConnection() *schema.Resource { Description: "Email domain of the connection", ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), }, + FieldSSOConnectionAdditionalEmailDomains: { + Type: schema.TypeList, + Optional: true, + Description: "Additional email domains that will be allowed to sign in via the connection", + MinItems: 1, + Elem: &schema.Schema{ + Required: false, + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), + }, + }, FieldSSOConnectionAAD: { Type: schema.TypeList, MaxItems: 1, @@ -141,6 +154,14 @@ func resourceCastaiSSOConnectionCreate(ctx context.Context, data *schema.Resourc EmailDomain: data.Get(FieldSSOConnectionEmailDomain).(string), } + if v, ok := data.Get(FieldSSOConnectionAdditionalEmailDomains).([]any); ok && len(v) > 0 { + var domains []string + for _, v := range v { + domains = append(domains, v.(string)) + } + req.AdditionalEmailDomains = toPtr(domains) + } + if v, ok := data.Get(FieldSSOConnectionAAD).([]any); ok && len(v) > 0 { req.Aad = toADConnector(v[0].(map[string]any)) } @@ -182,6 +203,9 @@ func resourceCastaiSSOConnectionRead(ctx context.Context, data *schema.ResourceD if err := data.Set(FieldSSOConnectionEmailDomain, connection.EmailDomain); err != nil { return diag.Errorf("setting email domain: %v", err) } + if err := data.Set(FieldSSOConnectionAdditionalEmailDomains, connection.AdditionalEmailDomains); err != nil { + return diag.Errorf("setting additional email domains: %v", err) + } return nil } @@ -190,6 +214,7 @@ func resourceCastaiSSOConnectionUpdate(ctx context.Context, data *schema.Resourc if !data.HasChanges( FieldSSOConnectionName, FieldSSOConnectionEmailDomain, + FieldSSOConnectionAdditionalEmailDomains, FieldSSOConnectionAAD, FieldSSOConnectionOkta, ) { @@ -206,6 +231,14 @@ func resourceCastaiSSOConnectionUpdate(ctx context.Context, data *schema.Resourc req.EmailDomain = toPtr(v.(string)) } + if v, ok := data.Get(FieldSSOConnectionAdditionalEmailDomains).([]any); ok && len(v) > 0 { + var domains []string + for _, v := range v { + domains = append(domains, v.(string)) + } + req.AdditionalEmailDomains = toPtr(domains) + } + if v, ok := data.Get(FieldSSOConnectionAAD).([]any); ok && len(v) > 0 { req.Aad = toADConnector(v[0].(map[string]any)) } diff --git a/castai/resource_sso_connection_test.go b/castai/resource_sso_connection_test.go index 81ca46cf..739a6977 100644 --- a/castai/resource_sso_connection_test.go +++ b/castai/resource_sso_connection_test.go @@ -11,8 +11,6 @@ import ( "testing" "time" - "github.com/castai/terraform-provider-castai/castai/sdk" - mock_sdk "github.com/castai/terraform-provider-castai/castai/sdk/mock" "github.com/golang/mock/gomock" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -20,6 +18,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/stretchr/testify/require" + + "github.com/castai/terraform-provider-castai/castai/sdk" + mock_sdk "github.com/castai/terraform-provider-castai/castai/sdk/mock" ) func TestAccResourceSSOConnection(t *testing.T) { @@ -50,46 +51,85 @@ func TestAccResourceSSOConnection(t *testing.T) { } func TestSSOConnection_ReadContext(t *testing.T) { - readBody := `{"id":"fce35ba2-5c06-4078-8391-1ac8f7ba798b","name":"test_sso","createdAt":"2023-11-02T10:49:14.376757Z","updatedAt":"2023-11-02T10:49:14.450828Z","emailDomain":"test_email","aad":{"adDomain":"test_connector","clientId":"test_client","clientSecret":"test_secret"}}` + t.Run("read azure ad connector", func(t *testing.T) { + t.Parallel() - mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) + readBody := `{"id":"fce35ba2-5c06-4078-8391-1ac8f7ba798b","name":"test_sso","createdAt":"2023-11-02T10:49:14.376757Z","updatedAt":"2023-11-02T10:49:14.450828Z","emailDomain":"test_email","additionalEmailDomains":[],"aad":{"adDomain":"test_connector","clientId":"test_client","clientSecret":"test_secret"}}` - connectionID := "fce35ba2-5c06-4078-8391-1ac8f7ba798b" + mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) - mockClient.EXPECT(). - SSOAPIGetSSOConnection(gomock.Any(), connectionID). - Return(&http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(readBody))), Header: map[string][]string{"Content-Type": {"json"}}}, nil) + connectionID := "fce35ba2-5c06-4078-8391-1ac8f7ba798b" - resource := resourceSSOConnection() - data := resource.Data( - terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal(connectionID), - }), 0)) + mockClient.EXPECT(). + SSOAPIGetSSOConnection(gomock.Any(), connectionID). + Return(&http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(readBody))), Header: map[string][]string{"Content-Type": {"json"}}}, nil) - result := resource.ReadContext(context.Background(), data, &ProviderConfig{ - api: &sdk.ClientWithResponses{ - ClientInterface: mockClient, - }, + resource := resourceSSOConnection() + data := resource.Data( + terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal(connectionID), + }), 0)) + + result := resource.ReadContext(context.Background(), data, &ProviderConfig{ + api: &sdk.ClientWithResponses{ + ClientInterface: mockClient, + }, + }) + + r := require.New(t) + r.Nil(result) + r.False(result.HasError()) + r.Equal("test_sso", data.Get(FieldSSOConnectionName)) + r.Equal("test_email", data.Get(FieldSSOConnectionEmailDomain)) + r.Empty(data.Get(FieldSSOConnectionAdditionalEmailDomains)) }) - r := require.New(t) - r.Nil(result) - r.False(result.HasError()) - r.Equal("test_sso", data.Get(FieldSSOConnectionName)) - r.Equal("test_email", data.Get(FieldSSOConnectionEmailDomain)) + t.Run("read azure ad connector with additional email domains", func(t *testing.T) { + t.Parallel() + + readBody := `{"id":"fce35ba2-5c06-4078-8391-1ac8f7ba798b","name":"test_sso","createdAt":"2023-11-02T10:49:14.376757Z","updatedAt":"2023-11-02T10:49:14.450828Z","emailDomain":"test_email","additionalEmailDomains":["domain.com", "other.com"],"aad":{"adDomain":"test_connector","clientId":"test_client","clientSecret":"test_secret"}}` + + mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) + + connectionID := "fce35ba2-5c06-4078-8391-1ac8f7ba798b" + + mockClient.EXPECT(). + SSOAPIGetSSOConnection(gomock.Any(), connectionID). + Return(&http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(readBody))), Header: map[string][]string{"Content-Type": {"json"}}}, nil) + + resource := resourceSSOConnection() + data := resource.Data( + terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal(connectionID), + }), 0)) + + result := resource.ReadContext(context.Background(), data, &ProviderConfig{ + api: &sdk.ClientWithResponses{ + ClientInterface: mockClient, + }, + }) + + r := require.New(t) + r.Nil(result) + r.False(result.HasError()) + r.Equal("test_sso", data.Get(FieldSSOConnectionName)) + r.Equal("test_email", data.Get(FieldSSOConnectionEmailDomain)) + r.Equal([]interface{}{"domain.com", "other.com"}, data.Get(FieldSSOConnectionAdditionalEmailDomains)) + }) } func TestSSOConnection_CreateADDConnector(t *testing.T) { - r := require.New(t) - mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) + t.Run("create azure ad connector", func(t *testing.T) { + r := require.New(t) + mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) - mockClient.EXPECT(). - SSOAPICreateSSOConnection(gomock.Any(), gomock.Any()). - DoAndReturn(func(_ context.Context, body sdk.SSOAPICreateSSOConnectionJSONBody) (*http.Response, error) { - got, err := json.Marshal(body) - r.NoError(err) + mockClient.EXPECT(). + SSOAPICreateSSOConnection(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, body sdk.SSOAPICreateSSOConnectionJSONBody) (*http.Response, error) { + got, err := json.Marshal(body) + r.NoError(err) - expected := []byte(`{ + expected := []byte(`{ "aad": { "adDomain": "test_connector", "clientId": "test_client", @@ -100,20 +140,20 @@ func TestSSOConnection_CreateADDConnector(t *testing.T) { } `) - equal, err := JSONBytesEqual(got, expected) - r.NoError(err) - r.True(equal, fmt.Sprintf("got: %v\n"+ - "expected: %v\n", string(got), string(expected))) + equal, err := JSONBytesEqual(got, expected) + r.NoError(err) + r.True(equal, fmt.Sprintf("got: %v\n"+ + "expected: %v\n", string(got), string(expected))) - return &http.Response{ - StatusCode: 200, - Header: map[string][]string{"Content-Type": {"json"}}, - Body: io.NopCloser(bytes.NewReader([]byte(`{"id": "b6bfc074-a267-400f-b8f1-db0850c369b1", "status": "STATUS_ACTIVE"}`))), - }, nil - }) + return &http.Response{ + StatusCode: 200, + Header: map[string][]string{"Content-Type": {"json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(`{"id": "b6bfc074-a267-400f-b8f1-db0850c369b1", "status": "STATUS_ACTIVE"}`))), + }, nil + }) - connectionID := "b6bfc074-a267-400f-b8f1-db0850c369b1" - readBody := io.NopCloser(bytes.NewReader([]byte(`{ + connectionID := "b6bfc074-a267-400f-b8f1-db0850c369b1" + readBody := io.NopCloser(bytes.NewReader([]byte(`{ "id": "b6bfc074-a267-400f-b8f1-db0850c369b1", "name": "test_sso", "createdAt": "2023-11-02T10:49:14.376757Z", @@ -126,34 +166,115 @@ func TestSSOConnection_CreateADDConnector(t *testing.T) { } }`))) - mockClient.EXPECT(). - SSOAPIGetSSOConnection(gomock.Any(), connectionID). - Return(&http.Response{StatusCode: 200, Body: readBody, Header: map[string][]string{"Content-Type": {"json"}}}, nil) - - resource := resourceSSOConnection() - data := resource.Data(terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{ - FieldSSOConnectionName: cty.StringVal("test_sso"), - FieldSSOConnectionEmailDomain: cty.StringVal("test_email"), - FieldSSOConnectionAAD: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - FieldSSOConnectionADDomain: cty.StringVal("test_connector"), - FieldSSOConnectionADClientID: cty.StringVal("test_client"), - FieldSSOConnectionADClientSecret: cty.StringVal("test_secret"), + mockClient.EXPECT(). + SSOAPIGetSSOConnection(gomock.Any(), connectionID). + Return(&http.Response{StatusCode: 200, Body: readBody, Header: map[string][]string{"Content-Type": {"json"}}}, nil) + + resource := resourceSSOConnection() + data := resource.Data(terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{ + FieldSSOConnectionName: cty.StringVal("test_sso"), + FieldSSOConnectionEmailDomain: cty.StringVal("test_email"), + FieldSSOConnectionAAD: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + FieldSSOConnectionADDomain: cty.StringVal("test_connector"), + FieldSSOConnectionADClientID: cty.StringVal("test_client"), + FieldSSOConnectionADClientSecret: cty.StringVal("test_secret"), + }), }), - }), - }), 0)) + }), 0)) - result := resource.CreateContext(context.Background(), data, &ProviderConfig{ - api: &sdk.ClientWithResponses{ - ClientInterface: mockClient, - }, + result := resource.CreateContext(context.Background(), data, &ProviderConfig{ + api: &sdk.ClientWithResponses{ + ClientInterface: mockClient, + }, + }) + + r.Nil(result) + r.False(result.HasError()) + r.Equal("test_sso", data.Get(FieldSSOConnectionName)) + r.Equal("test_email", data.Get(FieldSSOConnectionEmailDomain)) + equalADConnector(t, r, data.Get(FieldSSOConnectionAAD), "test_connector", "test_client", "test_secret") }) - r.Nil(result) - r.False(result.HasError()) - r.Equal("test_sso", data.Get(FieldSSOConnectionName)) - r.Equal("test_email", data.Get(FieldSSOConnectionEmailDomain)) - equalADConnector(t, r, data.Get(FieldSSOConnectionAAD), "test_connector", "test_client", "test_secret") + t.Run("create azure ad connector with additional email domains", func(t *testing.T) { + r := require.New(t) + mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) + + mockClient.EXPECT(). + SSOAPICreateSSOConnection(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, body sdk.SSOAPICreateSSOConnectionJSONBody) (*http.Response, error) { + got, err := json.Marshal(body) + r.NoError(err) + + expected := []byte(`{ + "aad": { + "adDomain": "test_connector", + "clientId": "test_client", + "clientSecret": "test_secret" + }, + "emailDomain": "test_email", + "additionalEmailDomains": ["test_domain1.com", "test_domain2.com"], + "name": "test_sso" +} +`) + + equal, err := JSONBytesEqual(got, expected) + r.NoError(err) + r.True(equal, fmt.Sprintf("got: %v\n"+ + "expected: %v\n", string(got), string(expected))) + + return &http.Response{ + StatusCode: 200, + Header: map[string][]string{"Content-Type": {"json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(`{"id": "b6bfc074-a267-400f-b8f1-db0850c369b1", "status": "STATUS_ACTIVE"}`))), + }, nil + }) + + connectionID := "b6bfc074-a267-400f-b8f1-db0850c369b1" + readBody := io.NopCloser(bytes.NewReader([]byte(`{ + "id": "b6bfc074-a267-400f-b8f1-db0850c369b1", + "name": "test_sso", + "createdAt": "2023-11-02T10:49:14.376757Z", + "updatedAt": "2023-11-02T10:49:14.450828Z", + "emailDomain": "test_email", + "additionalEmailDomains": ["test_domain1.com", "test_domain2.com"], + "aad": { + "adDomain": "test_connector", + "clientId": "test_client", + "clientSecret": "test_secret" + } +}`))) + + mockClient.EXPECT(). + SSOAPIGetSSOConnection(gomock.Any(), connectionID). + Return(&http.Response{StatusCode: 200, Body: readBody, Header: map[string][]string{"Content-Type": {"json"}}}, nil) + + resource := resourceSSOConnection() + data := resource.Data(terraform.NewInstanceStateShimmedFromValue(cty.ObjectVal(map[string]cty.Value{ + FieldSSOConnectionName: cty.StringVal("test_sso"), + FieldSSOConnectionEmailDomain: cty.StringVal("test_email"), + FieldSSOConnectionAdditionalEmailDomains: cty.ListVal([]cty.Value{cty.StringVal("test_domain1.com"), cty.StringVal("test_domain2.com")}), + FieldSSOConnectionAAD: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + FieldSSOConnectionADDomain: cty.StringVal("test_connector"), + FieldSSOConnectionADClientID: cty.StringVal("test_client"), + FieldSSOConnectionADClientSecret: cty.StringVal("test_secret"), + }), + }), + }), 0)) + + result := resource.CreateContext(context.Background(), data, &ProviderConfig{ + api: &sdk.ClientWithResponses{ + ClientInterface: mockClient, + }, + }) + + r.Nil(result) + r.False(result.HasError()) + r.Equal("test_sso", data.Get(FieldSSOConnectionName)) + r.Equal("test_email", data.Get(FieldSSOConnectionEmailDomain)) + equalADConnector(t, r, data.Get(FieldSSOConnectionAAD), "test_connector", "test_client", "test_secret") + }) } func TestSSOConnection_CreateOktaConnector(t *testing.T) { @@ -233,37 +354,38 @@ func TestSSOConnection_CreateOktaConnector(t *testing.T) { } func TestSSOConnection_UpdateADDConnector(t *testing.T) { - r := require.New(t) - mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) - - ctx := context.Background() - provider := &ProviderConfig{ - api: &sdk.ClientWithResponses{ - ClientInterface: mockClient, - }, - } - connectionID := "b6bfc074-a267-400f-b8f1-db0850c369b1" + t.Run("update azure ad connector", func(t *testing.T) { + r := require.New(t) + mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) + + ctx := context.Background() + provider := &ProviderConfig{ + api: &sdk.ClientWithResponses{ + ClientInterface: mockClient, + }, + } + connectionID := "b6bfc074-a267-400f-b8f1-db0850c369b1" - raw := make(map[string]interface{}) - raw[FieldSSOConnectionName] = "updated_name" + raw := make(map[string]interface{}) + raw[FieldSSOConnectionName] = "updated_name" - resource := resourceSSOConnection() - data := schema.TestResourceDataRaw(t, resource.Schema, raw) - data.SetId(connectionID) - r.NoError(data.Set(FieldSSOConnectionAAD, []map[string]interface{}{ - { - FieldSSOConnectionADDomain: "updated_domain", - FieldSSOConnectionADClientID: "updated_client_id", - FieldSSOConnectionADClientSecret: "updated_client_secret", - }, - })) + resource := resourceSSOConnection() + data := schema.TestResourceDataRaw(t, resource.Schema, raw) + data.SetId(connectionID) + r.NoError(data.Set(FieldSSOConnectionAAD, []map[string]interface{}{ + { + FieldSSOConnectionADDomain: "updated_domain", + FieldSSOConnectionADClientID: "updated_client_id", + FieldSSOConnectionADClientSecret: "updated_client_secret", + }, + })) - mockClient.EXPECT().SSOAPIUpdateSSOConnection(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ context.Context, _ string, body sdk.SSOAPIUpdateSSOConnectionJSONBody) (*http.Response, error) { - got, err := json.Marshal(body) - r.NoError(err) + mockClient.EXPECT().SSOAPIUpdateSSOConnection(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, body sdk.SSOAPIUpdateSSOConnectionJSONBody) (*http.Response, error) { + got, err := json.Marshal(body) + r.NoError(err) - expected := []byte(`{ + expected := []byte(`{ "aad": { "adDomain": "updated_domain", "clientId": "updated_client_id", @@ -272,12 +394,12 @@ func TestSSOConnection_UpdateADDConnector(t *testing.T) { "name": "updated_name" }`) - eq, err := JSONBytesEqual(got, expected) - r.NoError(err) - r.True(eq, fmt.Sprintf("got: %v\n"+ - "expected: %v\n", string(got), string(expected))) + eq, err := JSONBytesEqual(got, expected) + r.NoError(err) + r.True(eq, fmt.Sprintf("got: %v\n"+ + "expected: %v\n", string(got), string(expected))) - returnBody := []byte(`{ + returnBody := []byte(`{ "aad": { "adDomain": "updated_domain", "clientId": "updated_client_id", @@ -287,14 +409,14 @@ func TestSSOConnection_UpdateADDConnector(t *testing.T) { "name": "updated_name" }`) - return &http.Response{ - StatusCode: 200, - Header: map[string][]string{"Content-Type": {"json"}}, - Body: io.NopCloser(bytes.NewReader(returnBody)), - }, nil - }).Times(1) + return &http.Response{ + StatusCode: 200, + Header: map[string][]string{"Content-Type": {"json"}}, + Body: io.NopCloser(bytes.NewReader(returnBody)), + }, nil + }).Times(1) - readBody := io.NopCloser(bytes.NewReader([]byte(`{ + readBody := io.NopCloser(bytes.NewReader([]byte(`{ "id": "b6bfc074-a267-400f-b8f1-db0850c369b1", "name": "updated_name", "createdAt": "2023-11-02T10:49:14.376757Z", @@ -306,16 +428,109 @@ func TestSSOConnection_UpdateADDConnector(t *testing.T) { "clientSecret": "updated_client_secret" } }`))) - mockClient.EXPECT(). - SSOAPIGetSSOConnection(gomock.Any(), connectionID). - Return(&http.Response{StatusCode: 200, Body: readBody, Header: map[string][]string{"Content-Type": {"json"}}}, nil) + mockClient.EXPECT(). + SSOAPIGetSSOConnection(gomock.Any(), connectionID). + Return(&http.Response{StatusCode: 200, Body: readBody, Header: map[string][]string{"Content-Type": {"json"}}}, nil) - updateResult := resource.UpdateContext(ctx, data, provider) + updateResult := resource.UpdateContext(ctx, data, provider) - r.Nil(updateResult) - r.False(updateResult.HasError()) - r.Equal("updated_name", data.Get(FieldSSOConnectionName)) - equalADConnector(t, r, data.Get(FieldSSOConnectionAAD), "updated_domain", "updated_client_id", "updated_client_secret") + r.Nil(updateResult) + r.False(updateResult.HasError()) + r.Equal("updated_name", data.Get(FieldSSOConnectionName)) + equalADConnector(t, r, data.Get(FieldSSOConnectionAAD), "updated_domain", "updated_client_id", "updated_client_secret") + }) + + t.Run("update azure ad connector with additional email domains", func(t *testing.T) { + r := require.New(t) + mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t)) + + ctx := context.Background() + provider := &ProviderConfig{ + api: &sdk.ClientWithResponses{ + ClientInterface: mockClient, + }, + } + connectionID := "b6bfc074-a267-400f-b8f1-db0850c369b1" + + + raw := make(map[string]interface{}) + raw[FieldSSOConnectionName] = "updated_name" + + resource := resourceSSOConnection() + data := schema.TestResourceDataRaw(t, resource.Schema, raw) + data.SetId(connectionID) + r.NoError(data.Set(FieldSSOConnectionAAD, []map[string]interface{}{ + { + FieldSSOConnectionADDomain: "updated_domain", + FieldSSOConnectionADClientID: "updated_client_id", + FieldSSOConnectionADClientSecret: "updated_client_secret", + }, + })) + r.NoError(data.Set(FieldSSOConnectionAdditionalEmailDomains, []interface{}{"updated_domain_one", "updated_domain_two"})) + + mockClient.EXPECT().SSOAPIUpdateSSOConnection(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, body sdk.SSOAPIUpdateSSOConnectionJSONBody) (*http.Response, error) { + got, err := json.Marshal(body) + r.NoError(err) + + expected := []byte(`{ + "aad": { + "adDomain": "updated_domain", + "clientId": "updated_client_id", + "clientSecret": "updated_client_secret" + }, + "name": "updated_name", + "additionalEmailDomains": ["updated_domain_one", "updated_domain_two"] +}`) + + eq, err := JSONBytesEqual(got, expected) + r.NoError(err) + r.True(eq, fmt.Sprintf("got: %v\n"+ + "expected: %v\n", string(got), string(expected))) + + returnBody := []byte(`{ + "aad": { + "adDomain": "updated_domain", + "clientId": "updated_client_id", + "clientSecret": "updated_client_secret" + }, + "status": "STATUS_ACTIVE", + "name": "updated_name", + "additionalEmailDomains": ["updated_domain_one", "updated_domain_two"] +}`) + + return &http.Response{ + StatusCode: 200, + Header: map[string][]string{"Content-Type": {"json"}}, + Body: io.NopCloser(bytes.NewReader(returnBody)), + }, nil + }).Times(1) + + readBody := io.NopCloser(bytes.NewReader([]byte(`{ + "id": "b6bfc074-a267-400f-b8f1-db0850c369b1", + "name": "updated_name", + "createdAt": "2023-11-02T10:49:14.376757Z", + "updatedAt": "2023-11-02T10:49:14.450828Z", + "emailDomain": "test_email", + "additionalEmailDomains": ["updated_domain_one", "updated_domain_two"], + "aad": { + "adDomain": "updated_domain", + "clientId": "updated_client_id", + "clientSecret": "updated_client_secret" + } +}`))) + mockClient.EXPECT(). + SSOAPIGetSSOConnection(gomock.Any(), connectionID). + Return(&http.Response{StatusCode: 200, Body: readBody, Header: map[string][]string{"Content-Type": {"json"}}}, nil) + + updateResult := resource.UpdateContext(ctx, data, provider) + + r.Nil(updateResult) + r.False(updateResult.HasError()) + r.Equal("updated_name", data.Get(FieldSSOConnectionName)) + r.Equal([]interface{}{"updated_domain_one", "updated_domain_two"}, data.Get(FieldSSOConnectionAdditionalEmailDomains)) + equalADConnector(t, r, data.Get(FieldSSOConnectionAAD), "updated_domain", "updated_client_id", "updated_client_secret") + }) } func TestSSOConnection_UpdateOktaConnector(t *testing.T) { diff --git a/castai/sdk/api.gen.go b/castai/sdk/api.gen.go index aad5b2ae..e4f00acd 100644 --- a/castai/sdk/api.gen.go +++ b/castai/sdk/api.gen.go @@ -160,6 +160,18 @@ const ( NodeconfigV1ContainerRuntimeUnspecified NodeconfigV1ContainerRuntime = "unspecified" ) +// Defines values for NodeconfigV1ImageFamily. +const ( + AL2 NodeconfigV1ImageFamily = "AL2" + AL2023 NodeconfigV1ImageFamily = "AL2023" + Al2 NodeconfigV1ImageFamily = "al2" + Al2023 NodeconfigV1ImageFamily = "al2023" + BOTTLEROCKET NodeconfigV1ImageFamily = "BOTTLEROCKET" + Bottlerocket NodeconfigV1ImageFamily = "bottlerocket" + FAMILYUNSPECIFIED NodeconfigV1ImageFamily = "FAMILY_UNSPECIFIED" + FamilyUnspecified NodeconfigV1ImageFamily = "family_unspecified" +) + // Defines values for NodetemplatesV1AvailableInstanceTypeOs. const ( NodetemplatesV1AvailableInstanceTypeOsLinux NodetemplatesV1AvailableInstanceTypeOs = "linux" @@ -345,6 +357,11 @@ type CastaiInventoryV1beta1AddReservationResponse struct { Reservation *CastaiInventoryV1beta1ReservationDetails `json:"reservation,omitempty"` } +// CastaiInventoryV1beta1AttachableDisk defines model for castai.inventory.v1beta1.AttachableDisk. +type CastaiInventoryV1beta1AttachableDisk struct { + Name *string `json:"name,omitempty"` +} + // CastaiInventoryV1beta1AttachableGPUDevice defines model for castai.inventory.v1beta1.AttachableGPUDevice. type CastaiInventoryV1beta1AttachableGPUDevice struct { BlacklistedAt *time.Time `json:"blacklistedAt,omitempty"` @@ -644,11 +661,14 @@ type CastaiInventoryV1beta1InstanceReliability struct { // InstanceType is a cloud service provider specific VM type with basic data. type CastaiInventoryV1beta1InstanceType struct { - Architecture *string `json:"architecture,omitempty"` - BareMetal *bool `json:"bareMetal,omitempty"` - Burstable *bool `json:"burstable,omitempty"` - CastChoice *bool `json:"castChoice,omitempty"` - ComputeOptimized *bool `json:"computeOptimized,omitempty"` + Architecture *string `json:"architecture,omitempty"` + + // Contains a list of possible attachable disk types for the given instance types. Currently supported for GCP only. + AttachableDisks *[]CastaiInventoryV1beta1AttachableDisk `json:"attachableDisks,omitempty"` + BareMetal *bool `json:"bareMetal,omitempty"` + Burstable *bool `json:"burstable,omitempty"` + CastChoice *bool `json:"castChoice,omitempty"` + ComputeOptimized *bool `json:"computeOptimized,omitempty"` // Describes the manufacturers of the CPUs the instance type can be equipped with. CpuManufacturers *[]CastaiInventoryV1beta1InstanceTypeCPUManufacturer `json:"cpuManufacturers,omitempty"` @@ -1989,6 +2009,13 @@ type NodeconfigV1GetSuggestedConfigurationResponse struct { Subnets *[]NodeconfigV1SubnetDetails `json:"subnets,omitempty"` } +// List of supported image families to choose from. Values might be applicable to a specific cloud provider and will be rejected if the value is not supported. +// +// - AL2: Amazon Linux 2 (https://aws.amazon.com/amazon-linux-2/), EKS-specific. +// - AL2023: Amazon Linux 2023 (https://aws.amazon.com/linux/amazon-linux-2023/), EKS-specific. +// - BOTTLEROCKET: Bottlerocket (https://aws.amazon.com/bottlerocket/), EKS-specific. +type NodeconfigV1ImageFamily string + // NodeconfigV1KOPSConfig defines model for nodeconfig.v1.KOPSConfig. type NodeconfigV1KOPSConfig struct { // AWS key pair ID to be used for provisioned nodes. Has priority over sshPublicKey. @@ -2023,9 +2050,18 @@ type NodeconfigV1NewNodeConfiguration struct { Eks *NodeconfigV1EKSConfig `json:"eks,omitempty"` Gke *NodeconfigV1GKEConfig `json:"gke,omitempty"` - // Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Kubernetes version if possible. + // Image to be used while provisioning the node. + // Image setting takes precedence over image family. + // If both image and image family are empty, the latest image from a default family will be used, depending on the cloud provider. Image *string `json:"image"` + // List of supported image families to choose from. Values might be applicable to a specific cloud provider and will be rejected if the value is not supported. + // + // - AL2: Amazon Linux 2 (https://aws.amazon.com/amazon-linux-2/), EKS-specific. + // - AL2023: Amazon Linux 2023 (https://aws.amazon.com/linux/amazon-linux-2023/), EKS-specific. + // - BOTTLEROCKET: Bottlerocket (https://aws.amazon.com/bottlerocket/), EKS-specific. + ImageFamily *NodeconfigV1ImageFamily `json:"imageFamily,omitempty"` + // Init script to be run on your instance at launch. Should not contain any sensitive data. Value should be base64 encoded. InitScript *string `json:"initScript"` Kops *NodeconfigV1KOPSConfig `json:"kops,omitempty"` @@ -2081,9 +2117,18 @@ type NodeconfigV1NodeConfiguration struct { // The node configuration ID. Id *string `json:"id,omitempty"` - // Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Kubernetes version if possible. + // Image to be used while provisioning the node. + // Image setting takes precedence over image family. + // If both image and image family are empty, the latest image from a default family will be used, depending on the cloud provider. Image *string `json:"image"` + // List of supported image families to choose from. Values might be applicable to a specific cloud provider and will be rejected if the value is not supported. + // + // - AL2: Amazon Linux 2 (https://aws.amazon.com/amazon-linux-2/), EKS-specific. + // - AL2023: Amazon Linux 2023 (https://aws.amazon.com/linux/amazon-linux-2023/), EKS-specific. + // - BOTTLEROCKET: Bottlerocket (https://aws.amazon.com/bottlerocket/), EKS-specific. + ImageFamily *NodeconfigV1ImageFamily `json:"imageFamily,omitempty"` + // Base64 encoded init script to be run on your instance at launch. InitScript *string `json:"initScript"` Kops *NodeconfigV1KOPSConfig `json:"kops,omitempty"` @@ -2136,9 +2181,18 @@ type NodeconfigV1NodeConfigurationUpdate struct { Eks *NodeconfigV1EKSConfig `json:"eks,omitempty"` Gke *NodeconfigV1GKEConfig `json:"gke,omitempty"` - // Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Kubernetes version if possible. + // Image to be used while provisioning the node. + // Image setting takes precedence over image family. + // If both image and image family are empty, the latest image from a default family will be used, depending on the cloud provider. Image *string `json:"image"` + // List of supported image families to choose from. Values might be applicable to a specific cloud provider and will be rejected if the value is not supported. + // + // - AL2: Amazon Linux 2 (https://aws.amazon.com/amazon-linux-2/), EKS-specific. + // - AL2023: Amazon Linux 2023 (https://aws.amazon.com/linux/amazon-linux-2023/), EKS-specific. + // - BOTTLEROCKET: Bottlerocket (https://aws.amazon.com/bottlerocket/), EKS-specific. + ImageFamily *NodeconfigV1ImageFamily `json:"imageFamily,omitempty"` + // Init script to be run on your instance at launch. Should not contain any sensitive data. Value should be base64 encoded. InitScript *string `json:"initScript"` Kops *NodeconfigV1KOPSConfig `json:"kops,omitempty"` @@ -3001,6 +3055,9 @@ type ExternalClusterAPIGetCredentialsScriptParams struct { // Whether CAST AI Autoscaler components should be installed. // To enable backwards compatibility, when the field is omitted, it is defaulted to true. InstallAutoscalerAgent *bool `form:"installAutoscalerAgent,omitempty" json:"installAutoscalerAgent,omitempty"` + + // Whether CAST AI GPU metrics exporter should be installed + InstallGpuMetricsExporter *bool `form:"installGpuMetricsExporter,omitempty" json:"installGpuMetricsExporter,omitempty"` } // ExternalClusterAPIDisconnectClusterJSONBody defines parameters for ExternalClusterAPIDisconnectCluster. diff --git a/castai/sdk/client.gen.go b/castai/sdk/client.gen.go index f9da3337..796ec4df 100644 --- a/castai/sdk/client.gen.go +++ b/castai/sdk/client.gen.go @@ -4066,6 +4066,22 @@ func NewExternalClusterAPIGetCredentialsScriptRequest(server string, clusterId s } + if params.InstallGpuMetricsExporter != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "installGpuMetricsExporter", runtime.ParamLocationQuery, *params.InstallGpuMetricsExporter); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() req, err := http.NewRequest("GET", queryURL.String(), nil) diff --git a/docs/resources/sso_connection.md b/docs/resources/sso_connection.md index 6f6386eb..e5562667 100644 --- a/docs/resources/sso_connection.md +++ b/docs/resources/sso_connection.md @@ -34,6 +34,7 @@ resource "castai_sso_connection" "sso" { ### Optional - `aad` (Block List, Max: 1) Azure AD connector (see [below for nested schema](#nestedblock--aad)) +- `additional_email_domains` (List of String) Additional email domains that will be allowed to sign in via the connection - `okta` (Block List, Max: 1) Okta connector (see [below for nested schema](#nestedblock--okta)) - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))