diff --git a/examples/resources/cdp_environments_gcp_credential/resource.tf b/examples/resources/cdp_environments_gcp_credential/resource.tf new file mode 100644 index 00000000..9cbfba3b --- /dev/null +++ b/examples/resources/cdp_environments_gcp_credential/resource.tf @@ -0,0 +1,32 @@ +## 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. + +terraform { + required_providers { + cdp = { + source = "registry.terraform.io/cloudera/cdp" + } + } +} + +resource "cdp_environments_gcp_credential" "example" { + credential_name = "cdp-gcp-credential" + credential_key = "" + description = "Example GCP Credentials" +} + +output "credential_name" { + value = cdp_environments_gcp_credential.example.credential_name +} + +output "credential_key" { + value = cdp_environments_gcp_credential.example.credential_key + sensitive = true +} diff --git a/go.mod b/go.mod index 4f1f322c..95cf519c 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( github.com/hashicorp/terraform-registry-address v0.1.0 // indirect github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect diff --git a/go.sum b/go.sum index 291c0fcc..c548137f 100644 --- a/go.sum +++ b/go.sum @@ -307,6 +307,8 @@ github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKL github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM= +github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= @@ -630,6 +632,7 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/provider/provider.go b/provider/provider.go index 6d6ba168..f959b963 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -218,6 +218,7 @@ func (p *CdpProvider) Resources(_ context.Context) []func() resource.Resource { environments.NewAzureCredentialResource, environments.NewAzureEnvironmentResource, environments.NewGcpEnvironmentResource, + environments.NewGcpCredentialResource, datalake.NewAwsDatalakeResource, datalake.NewAzureDatalakeResource, iam.NewGroupResource, diff --git a/resources/environments/model_gcp_credential.go b/resources/environments/model_gcp_credential.go new file mode 100644 index 00000000..309adc37 --- /dev/null +++ b/resources/environments/model_gcp_credential.go @@ -0,0 +1,21 @@ +// 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 gcpCredentialResourceModel struct { + ID types.String `tfsdk:"id"` + CredentialName types.String `tfsdk:"credential_name"` + CredentialKey types.String `tfsdk:"credential_key"` + Crn types.String `tfsdk:"crn"` + Description types.String `tfsdk:"description"` +} diff --git a/resources/environments/resource_gcp_credential.go b/resources/environments/resource_gcp_credential.go new file mode 100644 index 00000000..2e384ffb --- /dev/null +++ b/resources/environments/resource_gcp_credential.go @@ -0,0 +1,152 @@ +// 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" + "encoding/base64" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client/operations" + environmentsmodels "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/models" + "github.com/cloudera/terraform-provider-cdp/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &gcpCredentialResource{} +) + +type gcpCredentialResource struct { + client *cdp.Client +} + +func NewGcpCredentialResource() resource.Resource { + return &gcpCredentialResource{} +} + +func (r *gcpCredentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_environments_gcp_credential" +} + +func (r *gcpCredentialResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = utils.GetCdpClientForResource(req, resp) +} + +func (r *gcpCredentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from data + var data gcpCredentialResourceModel + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + dec, err := base64.StdEncoding.DecodeString(data.CredentialKey.ValueString()) + + if err != nil { + diags.AddError("Unable to decode GCP credentials, please double check it.", + "Unable to decode GCP credential due to: "+err.Error()) + return + } + + credentialKey := string(dec) + + params := operations.NewCreateGCPCredentialParamsWithContext(ctx) + params.WithInput(&environmentsmodels.CreateGCPCredentialRequest{ + CredentialName: data.CredentialName.ValueStringPointer(), + Description: data.Description.ValueString(), + CredentialKey: &credentialKey, + }) + + responseOk, err := r.client.Environments.Operations.CreateGCPCredential(params) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "creating GCP Credential") + return + } + + data.Crn = types.StringPointerValue(responseOk.Payload.Credential.Crn) + data.ID = data.Crn + + // Save data into Terraform state + diags = resp.State.Set(ctx, data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *gcpCredentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state gcpCredentialResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get refreshed value from CDP + credentialName := state.CredentialName.ValueString() + params := operations.NewListCredentialsParamsWithContext(ctx) + params.WithInput(&environmentsmodels.ListCredentialsRequest{CredentialName: credentialName}) + listCredentialsResp, err := r.client.Environments.Operations.ListCredentials(params) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "reading GCP Credential") + return + } + + // Overwrite items with refreshed state + credentials := listCredentialsResp.GetPayload().Credentials + if len(credentials) == 0 || *credentials[0].CredentialName != credentialName { + resp.State.RemoveResource(ctx) // deleted + return + } + c := credentials[0] + + state.ID = types.StringPointerValue(c.Crn) + state.CredentialName = types.StringPointerValue(c.CredentialName) + state.Crn = types.StringPointerValue(c.Crn) + state.Description = types.StringValue(c.Description) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *gcpCredentialResource) Update(ctx context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { + tflog.Warn(ctx, "Update operation is not implemented yet.") +} + +func (r *gcpCredentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state gcpCredentialResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + credentialName := state.CredentialName.ValueString() + params := operations.NewDeleteCredentialParamsWithContext(ctx) + params.WithInput(&environmentsmodels.DeleteCredentialRequest{CredentialName: &credentialName}) + _, err := r.client.Environments.Operations.DeleteCredential(params) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "deleting GCP Credential") + return + } +} diff --git a/resources/environments/schema_gcp_credential.go b/resources/environments/schema_gcp_credential.go new file mode 100644 index 00000000..aa4ae6fa --- /dev/null +++ b/resources/environments/schema_gcp_credential.go @@ -0,0 +1,55 @@ +// 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/resource" + "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" +) + +func (r *gcpCredentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "The GCP credential is used for authorization to provision resources such as compute instances within your cloud provider account.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "credential_name": schema.StringAttribute{ + MarkdownDescription: "The name of the CDP credential.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "credential_key": schema.StringAttribute{ + MarkdownDescription: "The GCP credential JSON content encoded in Base64", + Required: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "crn": schema.StringAttribute{ + Computed: true, + }, + }, + } +} diff --git a/utils/file_reader.go b/utils/file_reader.go new file mode 100644 index 00000000..9eb94d93 --- /dev/null +++ b/utils/file_reader.go @@ -0,0 +1,30 @@ +// 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 utils + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" + "os" +) + +func ReadFileContent(ctx context.Context, path string) (*string, error) { + tflog.Info(ctx, fmt.Sprintf("About to read file on path: %s", path)) + data, err := os.ReadFile(path) + if err != nil { + tflog.Warn(ctx, fmt.Sprintf("Error occurred during file read: %s", err.Error())) + return nil, err + } + tflog.Info(ctx, "Reading file was successful.") + content := string(data) + return &content, nil +} diff --git a/utils/file_reader_test.go b/utils/file_reader_test.go new file mode 100644 index 00000000..91670e2a --- /dev/null +++ b/utils/file_reader_test.go @@ -0,0 +1,89 @@ +// 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 utils + +import ( + "context" + "log" + "os" + "runtime" + "testing" + + "github.com/hectane/go-acl" +) + +const FILE_NO_PERMISSIOM = os.FileMode(000) + +func TestWhenFileDoesNotExists(t *testing.T) { + content, err := ReadFileContent(context.TODO(), "someNonExistingStuff") + checkFailure(t, content, err) +} + +func TestReadFileContentWhenNoPermissionToRead(t *testing.T) { + file, err := os.CreateTemp(os.TempDir(), "gcp_read_temp") + if err != nil { + t.Errorf("Unable to prepare temprary file for testing due to: " + err.Error()) + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + log.Default().Printf("unable to clean up file ('%s') due to: %s", file.Name(), err.Error()) + } + }(file) + defer func(name string) { + err := os.Remove(name) + if err != nil { + log.Default().Printf("unable to clean up file ('%s') due to: %s", file.Name(), err.Error()) + } + }(file.Name()) + if runtime.GOOS == `windows` { + err = acl.Chmod(file.Name(), FILE_NO_PERMISSIOM) + } else { + err = os.Chmod(file.Name(), FILE_NO_PERMISSIOM) + } + if err != nil { + t.Errorf("Unable to update temprary file's permission for testing due to: " + err.Error()) + } + + content, err := ReadFileContent(context.TODO(), file.Name()) + + checkFailure(t, content, err) +} + +func TestReadFileContentWhenFileExistsAndHavePermission(t *testing.T) { + file, err := os.CreateTemp(os.TempDir(), "gcp_read_temp") + if err != nil { + t.Errorf("Unable to prepare temprary file for testing due to: " + err.Error()) + } + originalContent := []byte(`{"some":"amazing","content":true}`) + if _, err = file.Write(originalContent); err != nil { + t.Errorf("Unable to update temp file ('%s') content due to: %s\n", file.Name(), err.Error()) + } + + resultContent, err := ReadFileContent(context.TODO(), file.Name()) + + if err != nil { + t.Errorf("File read failed due to: %s", err.Error()) + } + originalContentToCompare := string(originalContent) + if *resultContent != originalContentToCompare { + t.Errorf("After file read it did not return the expected content! Expected: %s, got: %s", originalContentToCompare, *resultContent) + } +} + +func checkFailure(t *testing.T, content *string, readError error) { + if readError == nil { + t.Error("Expected read failure did not happen!") + } + if content != nil { + t.Error("Content should not be filled when error happen!") + } +}