From ada3edc63d2d06580908264f3a8cdb2a2a13dd21 Mon Sep 17 00:00:00 2001 From: David Szabo Date: Wed, 22 Nov 2023 09:53:40 +0100 Subject: [PATCH 1/2] CDPCP-10789 Environment Proxy Configuration Create/Read/Delete, unit tests --- provider/provider.go | 1 + provider/provider_test.go | 1 + .../environments/model_proxy_configuration.go | 25 ++ .../resource_proxy_configuration.go | 211 ++++++++++ .../resource_proxy_configuration_test.go | 385 ++++++++++++++++++ .../schema_proxy_configuration.go | 53 +++ 6 files changed, 676 insertions(+) create mode 100644 resources/environments/model_proxy_configuration.go create mode 100644 resources/environments/resource_proxy_configuration.go create mode 100644 resources/environments/resource_proxy_configuration_test.go create mode 100644 resources/environments/schema_proxy_configuration.go diff --git a/provider/provider.go b/provider/provider.go index 6d19f8da..8133e456 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -227,6 +227,7 @@ func (p *CdpProvider) Resources(_ context.Context) []func() resource.Resource { environments.NewAzureEnvironmentResource, environments.NewGcpEnvironmentResource, environments.NewGcpCredentialResource, + environments.NewProxyConfigurationResource, datalake.NewAwsDatalakeResource, datalake.NewAzureDatalakeResource, datalake.NewGcpDatalakeResource, diff --git a/provider/provider_test.go b/provider/provider_test.go index 946612db..e2523090 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -612,6 +612,7 @@ func TestCdpProvider_Resources(t *testing.T) { environments.NewAzureEnvironmentResource, environments.NewGcpEnvironmentResource, environments.NewGcpCredentialResource, + environments.NewProxyConfigurationResource, datalake.NewAwsDatalakeResource, datalake.NewAzureDatalakeResource, datalake.NewGcpDatalakeResource, diff --git a/resources/environments/model_proxy_configuration.go b/resources/environments/model_proxy_configuration.go new file mode 100644 index 00000000..4bc9c557 --- /dev/null +++ b/resources/environments/model_proxy_configuration.go @@ -0,0 +1,25 @@ +// Copyright 2023 Cloudera. All Rights Reserved. +// +// This file is licensed under the Apache License Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package environments + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type proxyConfigurationResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Protocol types.String `tfsdk:"protocol"` + Host types.String `tfsdk:"host"` + Port types.Int64 `tfsdk:"port"` + NoProxyHosts types.String `tfsdk:"no_proxy_hosts"` + User types.String `tfsdk:"user"` + Password types.String `tfsdk:"password"` +} diff --git a/resources/environments/resource_proxy_configuration.go b/resources/environments/resource_proxy_configuration.go new file mode 100644 index 00000000..29c79e5a --- /dev/null +++ b/resources/environments/resource_proxy_configuration.go @@ -0,0 +1,211 @@ +// Copyright 2023 Cloudera. All Rights Reserved. +// +// This file is licensed under the Apache License Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package environments + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client/operations" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/models" + "github.com/cloudera/terraform-provider-cdp/utils" +) + +var ( + _ resource.Resource = &proxyConfigurationResource{} +) + +type proxyConfigurationResource struct { + client *cdp.Client +} + +func NewProxyConfigurationResource() resource.Resource { + return &proxyConfigurationResource{} +} + +func (p *proxyConfigurationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_environments_proxy_configuration" +} + +func (p *proxyConfigurationResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = ProxyConfigurationSchema +} + +func (p *proxyConfigurationResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + p.client = utils.GetCdpClientForResource(req, resp) +} + +func (p *proxyConfigurationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data proxyConfigurationResourceModel + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + tflog.Error(ctx, "Got Error while trying to get plan") + return + } + + client := p.client.Environments + + params := operations.NewCreateProxyConfigParamsWithContext(ctx) + params.WithInput(&models.CreateProxyConfigRequest{ + ProxyConfigName: data.Name.ValueStringPointer(), + Description: data.Description.ValueString(), + Protocol: data.Protocol.ValueStringPointer(), + Host: data.Host.ValueStringPointer(), + Port: func(i int32) *int32 { return &i }(int32(data.Port.ValueInt64())), + NoProxyHosts: data.NoProxyHosts.ValueString(), + User: data.User.ValueString(), + Password: data.Password.ValueString(), + }) + + responseOk, err := client.Operations.CreateProxyConfig(params) + if err != nil { + msg := err.Error() + if d, ok := err.(utils.EnvironmentErrorPayload); ok && d.GetPayload() != nil { + msg = d.GetPayload().Message + } + + resp.Diagnostics.AddError( + "Create Proxy Configuration", + "Failed to create proxy configuration, unexpected error: "+msg, + ) + return + } + + proxyConfig := responseOk.Payload.ProxyConfig + data.ID = types.StringValue(*proxyConfig.Crn) + + diags = resp.State.Set(ctx, data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (p *proxyConfigurationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state proxyConfigurationResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + tflog.Error(ctx, "Got Error while trying to get state") + return + } + + client := p.client.Environments + + params := operations.NewDeleteProxyConfigParamsWithContext(ctx) + params.WithInput(&models.DeleteProxyConfigRequest{ProxyConfigName: state.Name.ValueStringPointer()}) + + _, err := client.Operations.DeleteProxyConfig(params) + if err != nil { + if envErr, ok := err.(*operations.DeleteProxyConfigDefault); ok { + if cdp.IsEnvironmentsError(envErr.GetPayload(), "NOT_FOUND", "") { + return + } else { + resp.Diagnostics.AddError( + "Delete Proxy Configuration", + "Failed to delete proxy configuration: "+envErr.Payload.Message, + ) + return + } + } + msg := err.Error() + if d, ok := err.(utils.EnvironmentErrorPayload); ok && d.GetPayload() != nil { + msg = d.GetPayload().Message + } + + resp.Diagnostics.AddError( + "Delete Proxy Configuration", + "Failed to delete proxy configuration, unexpected error: "+msg, + ) + } +} + +func (p *proxyConfigurationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state proxyConfigurationResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + tflog.Error(ctx, "Got Error while trying to get state") + return + } + + client := p.client.Environments + + params := operations.NewListProxyConfigsParamsWithContext(ctx) + params.WithInput(&models.ListProxyConfigsRequest{ProxyConfigName: state.Name.ValueString()}) + + responseOk, err := client.Operations.ListProxyConfigs(params) + if err != nil { + if envErr, ok := err.(*operations.DeleteProxyConfigDefault); ok { + if cdp.IsEnvironmentsError(envErr.GetPayload(), "NOT_FOUND", "") { + removeProxyConfigFromState(ctx, &resp.Diagnostics, &resp.State, state) + return + } else { + resp.Diagnostics.AddError( + "Read Proxy Configuration", + "Failed to read proxy configuration: "+envErr.Payload.Message, + ) + return + } + } + msg := err.Error() + if d, ok := err.(utils.EnvironmentErrorPayload); ok && d.GetPayload() != nil { + msg = d.GetPayload().Message + } + + resp.Diagnostics.AddError( + "Read Proxy Configuration", + "Failed to read proxy configuration, unexpected error: "+msg, + ) + return + } + + if len(responseOk.Payload.ProxyConfigs) == 0 { + removeProxyConfigFromState(ctx, &resp.Diagnostics, &resp.State, state) + return + } + + proxyConfig := responseOk.Payload.ProxyConfigs[0] + + state.Name = types.StringValue(*proxyConfig.ProxyConfigName) + state.Description = types.StringValue(proxyConfig.Description) + state.Protocol = types.StringValue(*proxyConfig.Protocol) + state.Host = types.StringValue(*proxyConfig.Host) + state.Port = types.Int64Value(int64(*proxyConfig.Port)) + state.NoProxyHosts = types.StringValue(proxyConfig.NoProxyHosts) + state.User = types.StringValue(proxyConfig.User) + state.Password = types.StringValue(proxyConfig.Password) + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func removeProxyConfigFromState(ctx context.Context, diag *diag.Diagnostics, state *tfsdk.State, model proxyConfigurationResourceModel) { + diag.AddWarning("Resource not found on provider", "Proxy configuration not found, removing resource from state.") + tflog.Warn(ctx, "Proxy configuration not found, removing resource from state", map[string]interface{}{ + "id": model.ID.ValueString(), + }) + state.RemoveResource(ctx) +} + +func (p *proxyConfigurationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddWarning("Update Proxy Configuration Not Supported", "Update proxy configuration is not supported. Plan changes were not applied.") +} diff --git a/resources/environments/resource_proxy_configuration_test.go b/resources/environments/resource_proxy_configuration_test.go new file mode 100644 index 00000000..e0ff194c --- /dev/null +++ b/resources/environments/resource_proxy_configuration_test.go @@ -0,0 +1,385 @@ +// Copyright 2023 Cloudera. All Rights Reserved. +// +// This file is licensed under the Apache License Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package environments + +import ( + "context" + "errors" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client/operations" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/models" + mocks "github.com/cloudera/terraform-provider-cdp/mocks/github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client/operations" +) + +func createRawProxyConfigResource(resourceID string) tftypes.Value { + return tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "description": tftypes.String, + "protocol": tftypes.String, + "host": tftypes.String, + "port": tftypes.Number, + "no_proxy_hosts": tftypes.String, + "user": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, resourceID), + "name": tftypes.NewValue(tftypes.String, "test-name"), + "description": tftypes.NewValue(tftypes.String, "test-description"), + "protocol": tftypes.NewValue(tftypes.String, "test-protocol"), + "host": tftypes.NewValue(tftypes.String, "test-host"), + "port": tftypes.NewValue(tftypes.Number, 99), + "no_proxy_hosts": tftypes.NewValue(tftypes.String, "test-npc"), + "user": tftypes.NewValue(tftypes.String, "test-user"), + "password": tftypes.NewValue(tftypes.String, "test-password"), + }, + ) +} + +func TestCreateProxyConfiguration(t *testing.T) { + testCases := map[string]struct { + expectedResponse interface{} + expectedErrorResponse interface{} + expectedError bool + expectedSummary string + expectedDetail string + expectedID string + expectedName string + expectedHost string + }{ + "OK": { + expectedResponse: &operations.CreateProxyConfigOK{ + Payload: &models.CreateProxyConfigResponse{ + ProxyConfig: &models.ProxyConfig{ + Crn: func(s string) *string { return &s }("test-pc-crn"), + Description: "test_description", + Host: func(s string) *string { return &s }("test-host"), + NoProxyHosts: "test-npc", + Password: "test-password", + Port: func(i int32) *int32 { return &i }(99), + Protocol: func(s string) *string { return &s }("test-protocol"), + ProxyConfigName: func(s string) *string { return &s }("test-name"), + User: "test-user", + }, + }, + }, + expectedErrorResponse: nil, + expectedError: false, + expectedSummary: "", + expectedDetail: "", + expectedID: "test-pc-crn", + expectedName: "test-name", + expectedHost: "test-host", + }, + "BadRequest": { + expectedResponse: nil, + expectedErrorResponse: &operations.CreateProxyConfigDefault{ + Payload: &models.Error{ + Code: "BAD_REQUEST", + Message: "Missing name field", + }, + }, + expectedError: true, + expectedSummary: "Create Proxy Configuration", + expectedDetail: "Failed to create proxy configuration, unexpected error: Missing name field", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + "TransportError": { + expectedResponse: nil, + expectedErrorResponse: errors.New("request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"), + expectedError: true, + expectedSummary: "Create Proxy Configuration", + expectedDetail: "Failed to create proxy configuration, unexpected error: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + ctx := context.TODO() + + mockClient := new(mocks.MockEnvironmentClientService) + + createMatcher := func(params *operations.CreateProxyConfigParams) bool { + match := *params.Input.ProxyConfigName == "test-name" + match = match && params.Input.Description == "test-description" + match = match && *params.Input.Protocol == "test-protocol" + match = match && *params.Input.Host == "test-host" + match = match && *params.Input.Port == 99 + match = match && params.Input.NoProxyHosts == "test-npc" + match = match && params.Input.User == "test-user" + match = match && params.Input.Password == "test-password" + return match + } + mockClient.On("CreateProxyConfig", mock.MatchedBy(createMatcher)).Return(testCase.expectedResponse, testCase.expectedErrorResponse) + + pcResource := &proxyConfigurationResource{ + client: &cdp.Client{Environments: NewMockEnvironments(mockClient)}, + } + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawProxyConfigResource(""), + Schema: ProxyConfigurationSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: ProxyConfigurationSchema, + }, + } + + pcResource.Create(ctx, req, resp) + + assert.Equal(t, testCase.expectedError, resp.Diagnostics.HasError()) + if testCase.expectedError { + assert.Equal(t, testCase.expectedSummary, resp.Diagnostics.Errors()[0].Summary()) + assert.Equal(t, testCase.expectedDetail, resp.Diagnostics.Errors()[0].Detail()) + } + + var state proxyConfigurationResourceModel + resp.State.Get(ctx, &state) + + assert.Equal(t, testCase.expectedID, state.ID.ValueString()) + assert.Equal(t, testCase.expectedName, state.Name.ValueString()) + assert.Equal(t, testCase.expectedHost, state.Host.ValueString()) + + mockClient.AssertExpectations(t) + }) + } +} + +func TestReadProxyConfiguration(t *testing.T) { + testCases := map[string]struct { + expectedResponse interface{} + expectedErrorResponse interface{} + expectedError bool + expectedSummary string + expectedDetail string + expectedID string + expectedName string + expectedHost string + }{ + "OK": { + expectedResponse: &operations.ListProxyConfigsOK{ + Payload: &models.ListProxyConfigsResponse{ + ProxyConfigs: []*models.ProxyConfig{ + { + Crn: func(s string) *string { return &s }("test-pc-crn"), + Description: "test_description", + Host: func(s string) *string { return &s }("test-host"), + NoProxyHosts: "test-npc", + Password: "test-password", + Port: func(i int32) *int32 { return &i }(99), + Protocol: func(s string) *string { return &s }("test-protocol"), + ProxyConfigName: func(s string) *string { return &s }("test-name"), + User: "test-user", + }, + }, + }, + }, + expectedErrorResponse: nil, + expectedError: false, + expectedSummary: "", + expectedDetail: "", + expectedID: "test-pc-crn", + expectedName: "test-name", + expectedHost: "test-host", + }, + "BadRequest": { + expectedResponse: nil, + expectedErrorResponse: &operations.ListProxyConfigsDefault{ + Payload: &models.Error{ + Code: "BAD_REQUEST", + Message: "Missing name field", + }, + }, + expectedError: true, + expectedSummary: "Read Proxy Configuration", + expectedDetail: "Failed to read proxy configuration, unexpected error: Missing name field", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + "TransportError": { + expectedResponse: nil, + expectedErrorResponse: errors.New("request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"), + expectedError: true, + expectedSummary: "Read Proxy Configuration", + expectedDetail: "Failed to read proxy configuration, unexpected error: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + ctx := context.TODO() + + mockClient := new(mocks.MockEnvironmentClientService) + + createMatcher := func(params *operations.ListProxyConfigsParams) bool { + return params.Input.ProxyConfigName == "test-name" + } + mockClient.On("ListProxyConfigs", mock.MatchedBy(createMatcher)).Return(testCase.expectedResponse, testCase.expectedErrorResponse) + + pcResource := &proxyConfigurationResource{ + client: &cdp.Client{Environments: NewMockEnvironments(mockClient)}, + } + + req := resource.ReadRequest{ + State: tfsdk.State{ + Raw: createRawProxyConfigResource("test-pc-crn"), + Schema: ProxyConfigurationSchema, + }, + } + + resp := &resource.ReadResponse{ + State: tfsdk.State{ + Schema: ProxyConfigurationSchema, + }, + } + + pcResource.Read(ctx, req, resp) + + assert.Equal(t, testCase.expectedError, resp.Diagnostics.HasError()) + if testCase.expectedError { + assert.Equal(t, testCase.expectedSummary, resp.Diagnostics.Errors()[0].Summary()) + assert.Equal(t, testCase.expectedDetail, resp.Diagnostics.Errors()[0].Detail()) + } + + var state proxyConfigurationResourceModel + resp.State.Get(ctx, &state) + + assert.Equal(t, testCase.expectedID, state.ID.ValueString()) + assert.Equal(t, testCase.expectedName, state.Name.ValueString()) + assert.Equal(t, testCase.expectedHost, state.Host.ValueString()) + + mockClient.AssertExpectations(t) + }) + } +} + +type deleteProxyConfigResponseBody struct{} + +func TestDeleteProxyConfiguration(t *testing.T) { + testCases := map[string]struct { + expectedResponse interface{} + expectedErrorResponse interface{} + expectedError bool + expectedSummary string + expectedDetail string + expectedID string + expectedName string + expectedHost string + }{ + "OK": { + expectedResponse: &operations.DeleteProxyConfigOK{ + Payload: deleteProxyConfigResponseBody{}, + }, + expectedErrorResponse: nil, + expectedError: false, + expectedSummary: "", + expectedDetail: "", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + "BadRequest": { + expectedResponse: nil, + expectedErrorResponse: &operations.DeleteProxyConfigDefault{ + Payload: &models.Error{ + Code: "BAD_REQUEST", + Message: "Missing name field", + }, + }, + expectedError: true, + expectedSummary: "Delete Proxy Configuration", + expectedDetail: "Failed to delete proxy configuration: Missing name field", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + "TransportError": { + expectedResponse: nil, + expectedErrorResponse: errors.New("request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"), + expectedError: true, + expectedSummary: "Delete Proxy Configuration", + expectedDetail: "Failed to delete proxy configuration, unexpected error: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)", + expectedID: "", + expectedName: "", + expectedHost: "", + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + ctx := context.TODO() + + mockClient := new(mocks.MockEnvironmentClientService) + + createMatcher := func(params *operations.DeleteProxyConfigParams) bool { + return *params.Input.ProxyConfigName == "test-name" + } + mockClient.On("DeleteProxyConfig", mock.MatchedBy(createMatcher)).Return(testCase.expectedResponse, testCase.expectedErrorResponse) + + pcResource := &proxyConfigurationResource{ + client: &cdp.Client{Environments: NewMockEnvironments(mockClient)}, + } + + req := resource.DeleteRequest{ + State: tfsdk.State{ + Raw: createRawProxyConfigResource("test-pc-crn"), + Schema: ProxyConfigurationSchema, + }, + } + + resp := &resource.DeleteResponse{ + State: tfsdk.State{ + Schema: ProxyConfigurationSchema, + }, + } + + pcResource.Delete(ctx, req, resp) + + assert.Equal(t, testCase.expectedError, resp.Diagnostics.HasError()) + if testCase.expectedError { + assert.Equal(t, testCase.expectedSummary, resp.Diagnostics.Errors()[0].Summary()) + assert.Equal(t, testCase.expectedDetail, resp.Diagnostics.Errors()[0].Detail()) + } + + var state proxyConfigurationResourceModel + resp.State.Get(ctx, &state) + + assert.Equal(t, testCase.expectedID, state.ID.ValueString()) + assert.Equal(t, testCase.expectedName, state.Name.ValueString()) + assert.Equal(t, testCase.expectedHost, state.Host.ValueString()) + assert.True(t, resp.State.Raw.IsNull()) + + mockClient.AssertExpectations(t) + }) + } +} diff --git a/resources/environments/schema_proxy_configuration.go b/resources/environments/schema_proxy_configuration.go new file mode 100644 index 00000000..61cfc0ff --- /dev/null +++ b/resources/environments/schema_proxy_configuration.go @@ -0,0 +1,53 @@ +// Copyright 2023 Cloudera. All Rights Reserved. +// +// This file is licensed under the Apache License Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package environments + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +var ProxyConfigurationSchema = schema.Schema{ + MarkdownDescription: "", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{ + Optional: true, + }, + "protocol": schema.StringAttribute{ + Required: true, + }, + "host": schema.StringAttribute{ + Required: true, + }, + "port": schema.Int64Attribute{ + Required: true, + }, + "no_proxy_hosts": schema.StringAttribute{ + Optional: true, + }, + "user": schema.StringAttribute{ + Optional: true, + }, + "password": schema.StringAttribute{ + Optional: true, + }, + }, +} From 712a668742af2aa97bdb203f45334c1110e9d173 Mon Sep 17 00:00:00 2001 From: David Szabo Date: Wed, 6 Dec 2023 14:36:04 +0100 Subject: [PATCH 2/2] CDPCP-10789 NoProxyHosts is a Set, Read extracted to a FindBy func --- .../environments/model_proxy_configuration.go | 2 +- .../resource_proxy_configuration.go | 74 +++++++++++++------ .../resource_proxy_configuration_test.go | 46 ++++++------ .../schema_proxy_configuration.go | 6 +- 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/resources/environments/model_proxy_configuration.go b/resources/environments/model_proxy_configuration.go index 4bc9c557..abb3ae93 100644 --- a/resources/environments/model_proxy_configuration.go +++ b/resources/environments/model_proxy_configuration.go @@ -19,7 +19,7 @@ type proxyConfigurationResourceModel struct { Protocol types.String `tfsdk:"protocol"` Host types.String `tfsdk:"host"` Port types.Int64 `tfsdk:"port"` - NoProxyHosts types.String `tfsdk:"no_proxy_hosts"` + NoProxyHosts types.Set `tfsdk:"no_proxy_hosts"` User types.String `tfsdk:"user"` Password types.String `tfsdk:"password"` } diff --git a/resources/environments/resource_proxy_configuration.go b/resources/environments/resource_proxy_configuration.go index 29c79e5a..989aa415 100644 --- a/resources/environments/resource_proxy_configuration.go +++ b/resources/environments/resource_proxy_configuration.go @@ -12,14 +12,18 @@ package environments import ( "context" + "errors" + "strings" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client/operations" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/models" "github.com/cloudera/terraform-provider-cdp/utils" @@ -49,6 +53,17 @@ func (p *proxyConfigurationResource) Configure(_ context.Context, req resource.C p.client = utils.GetCdpClientForResource(req, resp) } +func joinHostSet(hosts basetypes.SetValue) string { + if hosts.IsNull() || hosts.IsUnknown() { + return "" + } + str := make([]string, len(hosts.Elements())) + for i, elem := range hosts.Elements() { + str[i] = elem.(types.String).ValueString() + } + return strings.Join(str, ",") +} + func (p *proxyConfigurationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data proxyConfigurationResourceModel diags := req.Plan.Get(ctx, &data) @@ -67,7 +82,7 @@ func (p *proxyConfigurationResource) Create(ctx context.Context, req resource.Cr Protocol: data.Protocol.ValueStringPointer(), Host: data.Host.ValueStringPointer(), Port: func(i int32) *int32 { return &i }(int32(data.Port.ValueInt64())), - NoProxyHosts: data.NoProxyHosts.ValueString(), + NoProxyHosts: joinHostSet(data.NoProxyHosts), User: data.User.ValueString(), Password: data.Password.ValueString(), }) @@ -135,32 +150,29 @@ func (p *proxyConfigurationResource) Delete(ctx context.Context, req resource.De } } -func (p *proxyConfigurationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state proxyConfigurationResourceModel - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - tflog.Error(ctx, "Got Error while trying to get state") - return - } +func splitHostsToSet(hostsStr string, diags *diag.Diagnostics, ctx context.Context) basetypes.SetValue { + hosts := strings.Split(hostsStr, ",") - client := p.client.Environments + result, diag := types.SetValueFrom(ctx, types.StringType, hosts) + diags.Append(diag...) + return result +} +func FindProxyConfigurationByName(name string, ctx context.Context, client *client.Environments, diags *diag.Diagnostics) (*models.ProxyConfig, error) { params := operations.NewListProxyConfigsParamsWithContext(ctx) - params.WithInput(&models.ListProxyConfigsRequest{ProxyConfigName: state.Name.ValueString()}) + params.WithInput(&models.ListProxyConfigsRequest{ProxyConfigName: name}) responseOk, err := client.Operations.ListProxyConfigs(params) if err != nil { if envErr, ok := err.(*operations.DeleteProxyConfigDefault); ok { if cdp.IsEnvironmentsError(envErr.GetPayload(), "NOT_FOUND", "") { - removeProxyConfigFromState(ctx, &resp.Diagnostics, &resp.State, state) - return + return nil, errors.New("not found") } else { - resp.Diagnostics.AddError( + diags.AddError( "Read Proxy Configuration", "Failed to read proxy configuration: "+envErr.Payload.Message, ) - return + return nil, errors.New("unknown error") } } msg := err.Error() @@ -168,26 +180,46 @@ func (p *proxyConfigurationResource) Read(ctx context.Context, req resource.Read msg = d.GetPayload().Message } - resp.Diagnostics.AddError( + diags.AddError( "Read Proxy Configuration", "Failed to read proxy configuration, unexpected error: "+msg, ) - return + + return nil, errors.New("unknown error") } - if len(responseOk.Payload.ProxyConfigs) == 0 { - removeProxyConfigFromState(ctx, &resp.Diagnostics, &resp.State, state) + if len(responseOk.Payload.ProxyConfigs) > 0 { + return responseOk.Payload.ProxyConfigs[0], nil + } else { + return nil, errors.New("not found") + } +} + +func (p *proxyConfigurationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state proxyConfigurationResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + tflog.Error(ctx, "Got Error while trying to get state") return } - proxyConfig := responseOk.Payload.ProxyConfigs[0] + client := p.client.Environments + + proxyConfig, err := FindProxyConfigurationByName(state.Name.ValueString(), ctx, client, &resp.Diagnostics) + if err != nil { + if err.Error() == "not found" { + removeProxyConfigFromState(ctx, &resp.Diagnostics, &resp.State, state) + } + return + } state.Name = types.StringValue(*proxyConfig.ProxyConfigName) state.Description = types.StringValue(proxyConfig.Description) state.Protocol = types.StringValue(*proxyConfig.Protocol) state.Host = types.StringValue(*proxyConfig.Host) state.Port = types.Int64Value(int64(*proxyConfig.Port)) - state.NoProxyHosts = types.StringValue(proxyConfig.NoProxyHosts) + state.NoProxyHosts = splitHostsToSet(proxyConfig.NoProxyHosts, &diags, ctx) state.User = types.StringValue(proxyConfig.User) state.Password = types.StringValue(proxyConfig.Password) diff --git a/resources/environments/resource_proxy_configuration_test.go b/resources/environments/resource_proxy_configuration_test.go index e0ff194c..0773d6ad 100644 --- a/resources/environments/resource_proxy_configuration_test.go +++ b/resources/environments/resource_proxy_configuration_test.go @@ -31,27 +31,31 @@ func createRawProxyConfigResource(resourceID string) tftypes.Value { return tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "name": tftypes.String, - "description": tftypes.String, - "protocol": tftypes.String, - "host": tftypes.String, - "port": tftypes.Number, - "no_proxy_hosts": tftypes.String, - "user": tftypes.String, - "password": tftypes.String, + "id": tftypes.String, + "name": tftypes.String, + "description": tftypes.String, + "protocol": tftypes.String, + "host": tftypes.String, + "port": tftypes.Number, + "no_proxy_hosts": tftypes.Set{ + ElementType: tftypes.String, + }, + "user": tftypes.String, + "password": tftypes.String, }, }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, resourceID), - "name": tftypes.NewValue(tftypes.String, "test-name"), - "description": tftypes.NewValue(tftypes.String, "test-description"), - "protocol": tftypes.NewValue(tftypes.String, "test-protocol"), - "host": tftypes.NewValue(tftypes.String, "test-host"), - "port": tftypes.NewValue(tftypes.Number, 99), - "no_proxy_hosts": tftypes.NewValue(tftypes.String, "test-npc"), - "user": tftypes.NewValue(tftypes.String, "test-user"), - "password": tftypes.NewValue(tftypes.String, "test-password"), + "id": tftypes.NewValue(tftypes.String, resourceID), + "name": tftypes.NewValue(tftypes.String, "test-name"), + "description": tftypes.NewValue(tftypes.String, "test-description"), + "protocol": tftypes.NewValue(tftypes.String, "test-protocol"), + "host": tftypes.NewValue(tftypes.String, "test-host"), + "port": tftypes.NewValue(tftypes.Number, 99), + "no_proxy_hosts": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{tftypes.NewValue(tftypes.String, "test-npc1"), tftypes.NewValue(tftypes.String, "test-npc2")}), + "user": tftypes.NewValue(tftypes.String, "test-user"), + "password": tftypes.NewValue(tftypes.String, "test-password"), }, ) } @@ -74,7 +78,7 @@ func TestCreateProxyConfiguration(t *testing.T) { Crn: func(s string) *string { return &s }("test-pc-crn"), Description: "test_description", Host: func(s string) *string { return &s }("test-host"), - NoProxyHosts: "test-npc", + NoProxyHosts: "test-npc1,test-npc2", Password: "test-password", Port: func(i int32) *int32 { return &i }(99), Protocol: func(s string) *string { return &s }("test-protocol"), @@ -129,7 +133,7 @@ func TestCreateProxyConfiguration(t *testing.T) { match = match && *params.Input.Protocol == "test-protocol" match = match && *params.Input.Host == "test-host" match = match && *params.Input.Port == 99 - match = match && params.Input.NoProxyHosts == "test-npc" + match = match && params.Input.NoProxyHosts == "test-npc1,test-npc2" match = match && params.Input.User == "test-user" match = match && params.Input.Password == "test-password" return match @@ -192,7 +196,7 @@ func TestReadProxyConfiguration(t *testing.T) { Crn: func(s string) *string { return &s }("test-pc-crn"), Description: "test_description", Host: func(s string) *string { return &s }("test-host"), - NoProxyHosts: "test-npc", + NoProxyHosts: "test-npc1,test-npc2", Password: "test-password", Port: func(i int32) *int32 { return &i }(99), Protocol: func(s string) *string { return &s }("test-protocol"), diff --git a/resources/environments/schema_proxy_configuration.go b/resources/environments/schema_proxy_configuration.go index 61cfc0ff..aa3ef838 100644 --- a/resources/environments/schema_proxy_configuration.go +++ b/resources/environments/schema_proxy_configuration.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" ) var ProxyConfigurationSchema = schema.Schema{ @@ -40,8 +41,9 @@ var ProxyConfigurationSchema = schema.Schema{ "port": schema.Int64Attribute{ Required: true, }, - "no_proxy_hosts": schema.StringAttribute{ - Optional: true, + "no_proxy_hosts": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, }, "user": schema.StringAttribute{ Optional: true,