From 2241452d0ca58dd3555ce5676b812f254cd45a6d Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 12:20:19 +0530 Subject: [PATCH 01/18] new resource security certificate --- CHANGELOG.md | 1 + internal/interfaces/security_certificate.go | 250 +++++++++ internal/provider/provider.go | 1 + .../security/security_certificate_resource.go | 506 ++++++++++++++++++ 4 files changed, 758 insertions(+) create mode 100644 internal/interfaces/security_certificate.go create mode 100644 internal/provider/security/security_certificate_resource.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b855b5..eb343bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ FEATURES: * **New Resource:** `netapp-ontap_storage_qtrees` ([#82](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/82)) * **New Resource:** `netapp-ontap_qos_policies` ([#76](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/76)) * **New Resource:** `netapp-security_login_messages` ([#18](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/18)) +* **New Resource:** `netapp-ontap_security_certificate` ([#138](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/138)) ENHANCEMENTS: * **netapp-ontap_lun**: added `size_unit` option. ([#227](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/227)) diff --git a/internal/interfaces/security_certificate.go b/internal/interfaces/security_certificate.go new file mode 100644 index 00000000..d4970474 --- /dev/null +++ b/internal/interfaces/security_certificate.go @@ -0,0 +1,250 @@ +package interfaces + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/mitchellh/mapstructure" + "github.com/netapp/terraform-provider-netapp-ontap/internal/restclient" + "github.com/netapp/terraform-provider-netapp-ontap/internal/utils" +) + +// SecurityCertificateGetDataModelONTAP describes the GET record data model using go types for mapping. +type SecurityCertificateGetDataModelONTAP struct { + Name string `mapstructure:"name"` + UUID string `mapstructure:"uuid"` + CommonName string `mapstructure:"common_name"` + SVM svm `mapstructure:"svm"` + Scope string `mapstructure:"scope"` + Type string `mapstructure:"type"` + SerialNumber string `mapstructure:"serial_number"` + CA string `mapstructure:"ca"` + HashFunction string `mapstructure:"hash_function"` + KeySize int64 `mapstructure:"key_size"` + ExpiryTime string `mapstructure:"expiry_time"` + PublicCertificate string `mapstructure:"public_certificate"` +} + +// SignedSecurityCertificateGetDataModelONTAP describes the GET record data model using go types for mapping. +type SignedSecurityCertificateGetDataModelONTAP struct { + SignedCertificate string `mapstructure:"public_certificate"` +} + +// SecurityCertificateResourceCreateBodyDataModelONTAP describes the create/install body data model using go types for mapping. +type SecurityCertificateResourceCreateBodyDataModelONTAP struct { + Name string `mapstructure:"name,omitempty"` + CommonName string `mapstructure:"common_name"` + Type string `mapstructure:"type"` + SVM svm `mapstructure:"svm,omitempty"` + Scope string `mapstructure:"scope,omitempty"` + PublicCertificate string `mapstructure:"public_certificate,omitempty"` + PrivateKey string `mapstructure:"private_key,omitempty"` + HashFunction string `mapstructure:"hash_function,omitempty"` + KeySize int64 `mapstructure:"key_size,omitempty"` + ExpiryTime string `mapstructure:"expiry_time,omitempty"` +} + +// SecurityCertificateResourceSignBodyDataModelONTAP describes the signing body data model using go types for mapping. +type SecurityCertificateResourceSignBodyDataModelONTAP struct { + SigningRequest string `mapstructure:"signing_request"` + HashFunction string `mapstructure:"hash_function,omitempty"` + ExpiryTime string `mapstructure:"expiry_time,omitempty"` +} + +// SecurityCertificateDataSourceFilterModel describes the data source data model for queries. +type SecurityCertificateDataSourceFilterModel struct { + SVMName string `mapstructure:"svm.name"` + Scope string `mapstructure:"scope"` + CommonName string `mapstructure:"common_name"` + Type string `mapstructure:"type"` +} + +// GetSecurityCertificate to get security_certificate info +// Retrieves the certificate with the given name and/or (common name & type) +func GetSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, name string, common_name string, type_ string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + query := r.NewQuery() + if name != "" { + query.Set("name", name) + } else { + query.Set("common_name", common_name) + query.Set("type", type_) + } + var fields = []string{"uuid", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"} + if version.Generation == 9 && version.Major >= 8 { + fields = append(fields, "name") + } + query.Fields(fields) + + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + if strings.Contains(err.Error(), "or more records when only one is expected") { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", "Duplicate records found with the same common_name.") + } + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response, &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificate data source: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// GetSecurityCertificateByName to get security_certificate info +// Retrieves the certificate using its unique name +func GetSecurityCertificateByName(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, name string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + query := r.NewQuery() + if version.Generation == 9 && version.Major >= 8 { + query.Add("name", name) + } else { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", "Attribute 'name' requires ONTAP 9.8 or later.") + } + query.Fields([]string{"uuid", "name", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"}) + + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response, &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificate data source: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// GetSecurityCertificateByUUID to get security_certificate info +// Retrieves the certificate using its UUID +func GetSecurityCertificateByUUID(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, uuid string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates/" + uuid + query := r.NewQuery() + var fields = []string{"uuid", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"} + if version.Generation == 9 && version.Major >= 8 { + fields = append(fields, "name") + } + query.Fields(fields) + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response, &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificate data source: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// GetSecurityCertificates to get security_certificate info for all resources matching a filter +func GetSecurityCertificates(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, filter *SecurityCertificateDataSourceFilterModel) ([]SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + query := r.NewQuery() + var fields = []string{"uuid", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"} + if version.Generation == 9 && version.Major >= 8 { + fields = append(fields, "name") + } + query.Fields(fields) + + if filter != nil { + var filterMap map[string]interface{} + if err := mapstructure.Decode(filter, &filterMap); err != nil { + return nil, errorHandler.MakeAndReportError("error encoding security_certificates filter info", fmt.Sprintf("error on filter %#v: %s", filter, err)) + } + query.SetValues(filterMap) + } + + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("security certificates filter: %+v", query)) + statusCode, response, err := r.GetZeroOrMoreRecords(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + return nil, errorHandler.MakeAndReportError("error reading security_certificates info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP []SecurityCertificateGetDataModelONTAP + for _, info := range response { + var record SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(info, &record); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, info %#v", err, statusCode, info)) + } + dataONTAP = append(dataONTAP, record) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificates data source: %#v", dataONTAP)) + return dataONTAP, nil +} + +// CreateOrInstallSecurityCertificate to create/ install a security certificate +func CreateOrInstallSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, body SecurityCertificateResourceCreateBodyDataModelONTAP, operation string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return nil, errorHandler.MakeAndReportError("error encoding security certificate body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body)) + } + query := r.NewQuery() + query.Add("return_records", "true") + + statusCode, response, err := r.CallCreateMethod(api, query, bodyMap) + if err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("error %s security certificate", operation), fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response.Records[0], &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError("error decoding security certificate info", fmt.Sprintf("error on decode storage/security_certificatess info: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Created security certificate: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// SignSecurityCertificate to sign a security_certificate +func SignSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string, body SecurityCertificateResourceSignBodyDataModelONTAP) (*SignedSecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return nil, errorHandler.MakeAndReportError("error encoding security certificate body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body)) + } + query := r.NewQuery() + query.Add("return_records", "true") + + statusCode, response, err := r.CallCreateMethod(api+"/"+uuid+"/sign", query, bodyMap) + if err != nil { + return nil, errorHandler.MakeAndReportError("error signing security certificate", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SignedSecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response.Records[0], &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError("error decoding signed security certificate info", fmt.Sprintf("error on decode storage/security_certificatess/{ca.uuid}/sign info: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Signed security certificate: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// DeleteSecurityCertificate to delete a security_certificate +func DeleteSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string) error { + api := "security/certificates" + statusCode, _, err := r.CallDeleteMethod(api+"/"+uuid, nil, nil) + if err != nil { + return errorHandler.MakeAndReportError("error deleting security certificate", fmt.Sprintf("error on DELETE %s: %s, statusCode %d", api, err, statusCode)) + } + return nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ee52c092..f015c62c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -236,6 +236,7 @@ func (p *ONTAPProvider) Resources(ctx context.Context) []func() resource.Resourc protocols.NewProtocolsSanIgroupResource, protocols.NewProtocolsSanLunMapsResource, security.NewSecurityAccountResource, + security.NewSecurityCertificateResource, security.NewSecurityRolesResource, security.NewSecurityLoginMessageResource, snapmirror.NewSnapmirrorResource, diff --git a/internal/provider/security/security_certificate_resource.go b/internal/provider/security/security_certificate_resource.go new file mode 100644 index 00000000..9f6a20df --- /dev/null +++ b/internal/provider/security/security_certificate_resource.go @@ -0,0 +1,506 @@ +package security + +import ( + "context" + "fmt" + "strings" + + "github.com/netapp/terraform-provider-netapp-ontap/internal/provider/connection" + + "github.com/hashicorp/terraform-plugin-framework/path" + "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/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/netapp/terraform-provider-netapp-ontap/internal/interfaces" + "github.com/netapp/terraform-provider-netapp-ontap/internal/utils" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &SecurityCertificateResource{} +var _ resource.ResourceWithImportState = &SecurityCertificateResource{} + +// NewSecurityCertificateResource is a helper function to simplify the provider implementation. +func NewSecurityCertificateResource() resource.Resource { + return &SecurityCertificateResource{ + config: connection.ResourceOrDataSourceConfig{ + Name: "security_certificate", + }, + } +} + +// SecurityCertificateResource defines the resource implementation. +type SecurityCertificateResource struct { + config connection.ResourceOrDataSourceConfig +} + +// SecurityCertificateResourceModel describes the resource data model. +type SecurityCertificateResourceModel struct { + CxProfileName types.String `tfsdk:"cx_profile_name"` + Name types.String `tfsdk:"name"` + CommonName types.String `tfsdk:"common_name"` + Type types.String `tfsdk:"type"` + SVMName types.String `tfsdk:"svm_name"` + Scope types.String `tfsdk:"scope"` + SerialNumber types.String `tfsdk:"serial_number"` + CA types.String `tfsdk:"ca"` + PublicCertificate types.String `tfsdk:"public_certificate"` + SignedCertificate types.String `tfsdk:"signed_certificate"` + PrivateKey types.String `tfsdk:"private_key"` + SigningRequest types.String `tfsdk:"signing_request"` + HashFunction types.String `tfsdk:"hash_function"` + KeySize types.Int64 `tfsdk:"key_size"` + ExpiryTime types.String `tfsdk:"expiry_time"` + ID types.String `tfsdk:"id"` +} + +// Metadata returns the resource type name. +func (r *SecurityCertificateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + r.config.Name +} + +// Schema defines the schema for the resource. +func (r *SecurityCertificateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "SecurityCertificate resource", + + Attributes: map[string]schema.Attribute{ + "cx_profile_name": schema.StringAttribute{ + MarkdownDescription: "Connection profile name.", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The unique name of the security certificate per SVM.", + Optional: true, + Computed: true, + }, + "common_name": schema.StringAttribute{ + MarkdownDescription: "Common name of the certificate.", + Required: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "Type of certificate.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("client", "server", "client_ca", "server_ca", "root_ca"), + }, + }, + "svm_name": schema.StringAttribute{ + MarkdownDescription: "Name of the SVM in which the certificate is created or installed or the SVM on which the signed certificate will exist.", + Optional: true, + }, + "scope": schema.StringAttribute{ + MarkdownDescription: "Set to 'svm' for certificates installed in a SVM. Otherwise, set to 'cluster'.", + Optional: false, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "serial_number": schema.StringAttribute{ + MarkdownDescription: "Serial number of the certificate.", + Optional: false, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "ca": schema.StringAttribute{ + MarkdownDescription: "Certificate authority.", + Optional: false, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "public_certificate": schema.StringAttribute{ + MarkdownDescription: "Public key Certificate in PEM format. If this is not provided during create action, a self-signed certificate is created.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "signed_certificate": schema.StringAttribute{ + MarkdownDescription: "Signed public key Certificate in PEM format that is returned while signing a certificate.", + Optional: false, + Computed: true, + }, + "private_key": schema.StringAttribute{ + MarkdownDescription: "Private key Certificate in PEM format. Only valid when installing a CA-signed certificate.", + Optional: true, + Sensitive: true, + }, + "signing_request": schema.StringAttribute{ + MarkdownDescription: "Certificate signing request to be signed by the given certificate authority. Request should be in X509 PEM format.", + Optional: true, + }, + "hash_function": schema.StringAttribute{ + MarkdownDescription: "Hashing function.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("sha1", "sha256", "md5", "sha224", "sha384", "sha512"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "key_size": schema.Int64Attribute{ + MarkdownDescription: "Key size of the certificate in bits.", + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.OneOf(512, 1024, 1536, 2048, 3072), + }, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "expiry_time": schema.StringAttribute{ + MarkdownDescription: "Certificate expiration time, in ISO 8601 duration format or date and time format.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "UUID of the certificate.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *SecurityCertificateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + config, ok := req.ProviderData.(connection.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + } + r.config.ProviderConfig = config +} + +// Read refreshes the Terraform state with the latest data. +func (r *SecurityCertificateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data SecurityCertificateResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + // we need to defer setting the client until we can read the connection profile name + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + cluster, err := interfaces.GetCluster(errorHandler, *client) + if err != nil { + // error reporting done inside GetCluster + return + } + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "cluster not found") + return + } + + var restInfo *interfaces.SecurityCertificateGetDataModelONTAP + if data.ID.ValueString() != "" { + restInfo, err = interfaces.GetSecurityCertificateByUUID(errorHandler, *client, cluster.Version, data.ID.ValueString()) + if err != nil { + // error reporting done inside GetSecurityCertificateByUUID + return + } + } else { + restInfo, err = interfaces.GetSecurityCertificate(errorHandler, *client, cluster.Version, data.Name.ValueString(), data.CommonName.ValueString(), data.Type.ValueString()) + if err != nil { + // error reporting done inside GetSecurityCertificate + return + } + } + + if restInfo == nil { + errorHandler.MakeAndReportError("error reading info", "No Certificate found") + return + } + + // Set the values from the response into the data model + data.ID = types.StringValue(restInfo.UUID) + data.Name = types.StringValue(restInfo.Name) + data.CommonName = types.StringValue(restInfo.CommonName) + data.Scope = types.StringValue(restInfo.Scope) + data.Type = types.StringValue(restInfo.Type) + data.SerialNumber = types.StringValue(restInfo.SerialNumber) + data.CA = types.StringValue(restInfo.CA) + data.HashFunction = types.StringValue(restInfo.HashFunction) + data.KeySize = types.Int64Value(restInfo.KeySize) + data.PublicCertificate = types.StringValue(restInfo.PublicCertificate) + if data.ExpiryTime.IsNull() { + data.ExpiryTime = types.StringValue(restInfo.ExpiryTime) + } + if data.SVMName.IsNull() { + data.SVMName = types.StringValue(restInfo.SVM.Name) + } + + // Write logs using the tflog package + // Documentation: https://terraform.io/plugin/log + tflog.Debug(ctx, fmt.Sprintf("read a resource: %#v", data)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Create a resource and retrieve UUID +func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *SecurityCertificateResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if !data.SigningRequest.IsNull() { + // this if block is for signing security certificate + var body interfaces.SecurityCertificateResourceSignBodyDataModelONTAP + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + cluster, err := interfaces.GetCluster(errorHandler, *client) + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "Cluster not found.") + return + } + if err != nil { + // error reporting done inside GetCluster + return + } + + // Read the updated data from the API + restInfo, err := interfaces.GetSecurityCertificate(errorHandler, *client, cluster.Version, data.Name.ValueString(), data.CommonName.ValueString(), data.Type.ValueString()) + if err != nil { + // error reporting done inside GetSecurityCertificate + return + } + + data.ID = types.StringValue(restInfo.UUID) + data.Name = types.StringValue(restInfo.Name) + data.CommonName = types.StringValue(restInfo.CommonName) + data.Type = types.StringValue(restInfo.Type) + data.Scope = types.StringValue(restInfo.Scope) + data.SerialNumber = types.StringValue(restInfo.SerialNumber) + data.CA = types.StringValue(restInfo.CA) + data.PublicCertificate = types.StringValue(restInfo.PublicCertificate) + data.HashFunction = types.StringValue(restInfo.HashFunction) + data.KeySize = types.Int64Value(restInfo.KeySize) + + body.SigningRequest = data.SigningRequest.ValueString() + if !data.HashFunction.IsUnknown() { + body.HashFunction = data.HashFunction.ValueString() + } + if !data.ExpiryTime.IsUnknown() { + body.ExpiryTime = data.ExpiryTime.ValueString() + } + + resource, err := interfaces.SignSecurityCertificate(errorHandler, *client, restInfo.UUID, body) + if err != nil { + // error reporting done inside SignSecurityCertificate + return + } + + // Save public_certificate returned while signing certificate into Terraform state + data.SignedCertificate = types.StringValue(resource.SignedCertificate) + + tflog.Trace(ctx, "signed a resource") + } else { + // This else block is for creating or installing security certificate + var body interfaces.SecurityCertificateResourceCreateBodyDataModelONTAP + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + cluster, err := interfaces.GetCluster(errorHandler, *client) + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "Cluster not found.") + return + } + if err != nil { + // error reporting done inside GetCluster + return + } + + if !data.Name.IsUnknown() { + if cluster.Version.Generation == 9 && cluster.Version.Major >= 8 { + body.Name = data.Name.ValueString() + } else { + tflog.Error(ctx, fmt.Sprintf("'name' is supported with ONTAP 9.8 or higher.")) + } + } + body.CommonName = data.CommonName.ValueString() + body.Type = data.Type.ValueString() + if !data.SVMName.IsUnknown() { + body.SVM.Name = data.SVMName.ValueString() + } + if !data.PublicCertificate.IsUnknown() { + body.PublicCertificate = data.PublicCertificate.ValueString() + } + if !data.PrivateKey.IsUnknown() { + body.PrivateKey = data.PrivateKey.ValueString() + } + if !data.HashFunction.IsUnknown() { + body.HashFunction = data.HashFunction.ValueString() + } + if !data.KeySize.IsUnknown() { + body.KeySize = data.KeySize.ValueInt64() + } + if !data.ExpiryTime.IsUnknown() { + body.ExpiryTime = data.ExpiryTime.ValueString() + } + + var operation string + if !data.PublicCertificate.IsUnknown() || !data.PrivateKey.IsUnknown() { + operation = "signing" + } else { + operation = "creating" + } + resource, err := interfaces.CreateOrInstallSecurityCertificate(errorHandler, *client, body, operation) + if err != nil { + // error reporting done inside CreateOrInstallSecurityCertificate + return + } + tflog.Trace(ctx, "created/ installed a resource") + data.ID = types.StringValue(resource.UUID) + + // Read the updated data from the API + restInfo, err := interfaces.GetSecurityCertificateByUUID(errorHandler, *client, cluster.Version, resource.UUID) + if err != nil { + // error reporting done inside GetSecurityCertificateByUUID + return + } + + data.Name = types.StringValue(restInfo.Name) + data.CommonName = types.StringValue(restInfo.CommonName) + data.Type = types.StringValue(restInfo.Type) + data.SVMName = types.StringValue(restInfo.SVM.Name) + data.Scope = types.StringValue(restInfo.Scope) + data.PublicCertificate = types.StringValue(restInfo.PublicCertificate) + data.SerialNumber = types.StringValue(restInfo.SerialNumber) + data.CA = types.StringValue(restInfo.CA) + data.HashFunction = types.StringValue(restInfo.HashFunction) + data.KeySize = types.Int64Value(restInfo.KeySize) + if data.ExpiryTime.IsUnknown() { + data.ExpiryTime = types.StringValue(restInfo.ExpiryTime) + } + // SignedCertificate would be available only while signing a certificate + data.SignedCertificate = types.StringValue("NA") + + tflog.Trace(ctx, "read newly created resource") + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *SecurityCertificateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *SecurityCertificateResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Add an error diagnostic indicating that Update is not supported + resp.Diagnostics.AddError( + "Update Not Supported", + "The update operation is not supported for the security_certificate resource.", + ) + tflog.Error(ctx, "Update not supported for resource security_certificate") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *SecurityCertificateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *SecurityCertificateResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + if data.ID.IsUnknown() { + errorHandler.MakeAndReportError("UUID is null", "security certificate UUID is null") + return + } + + err = interfaces.DeleteSecurityCertificate(errorHandler, *client, data.ID.ValueString()) + if err != nil { + return + } + +} + +// ImportState imports a resource using ID from terraform import command by calling the Read method. +func (r *SecurityCertificateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, fmt.Sprintf("import req security certificate resource: %#v", req)) + // Parse the ID + idParts := strings.Split(req.ID, ",") + + // import name and cx_profile + if len(idParts) == 2 { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[1])...) + return + } + + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: name,cx_profile_name. Got: %q", req.ID), + ) +} From d96d857ceb563b25b78fd662f0019b3573bed3dd Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 12:21:36 +0530 Subject: [PATCH 02/18] acc test for security_certificate resource --- .../security_certificate_resource_test.go | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 internal/provider/security/security_certificate_resource_test.go diff --git a/internal/provider/security/security_certificate_resource_test.go b/internal/provider/security/security_certificate_resource_test.go new file mode 100644 index 00000000..675b471a --- /dev/null +++ b/internal/provider/security/security_certificate_resource_test.go @@ -0,0 +1,67 @@ +package security_test + +import ( + "fmt" + "os" + "testing" + + ntest "github.com/netapp/terraform-provider-netapp-ontap/internal/provider" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccSecurityCertificateResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { ntest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ntest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create security certificate and read + { + Config: testAccSecurityCertificateResourceCertificateConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "tfsvm_ca_cert1"), + ), + }, + // Import and read + { + ResourceName: "netapp-ontap_security_certificate.example", + ImportState: true, + ImportStateId: fmt.Sprintf("%s,%s", "tfsvm_ca_cert1", "cluster1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "tfsvm_ca_cert1"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccSecurityCertificateResourceCertificateConfig() string { + host := os.Getenv("TF_ACC_NETAPP_HOST") + admin := os.Getenv("TF_ACC_NETAPP_USER") + password := os.Getenv("TF_ACC_NETAPP_PASS") + if host == "" || admin == "" || password == "" { + fmt.Println("TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests") + os.Exit(1) + } + return fmt.Sprintf(` +provider "netapp-ontap" { + connection_profiles = [ + { + name = "cluster1" + hostname = "%s" + username = "%s" + password = "%s" + validate_certs = false + }, + ] +} + +resource "netapp-ontap_security_certificate" "example" { + cx_profile_name = "cluster1" + name = "tfsvm_ca_cert1" + common_name = "tfsvm_ca_cert" + type = "root_ca" + svm_name = "tfsvm" +}`, host, admin, password) +} From 55d6f980d79d2c6f4015b95e22335a9f9cf87f12 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 12:35:12 +0530 Subject: [PATCH 03/18] added examples --- .../provider.tf | 1 + .../resource.tf | 44 +++++++++++++++++++ .../terraform.tfvars | 1 + .../variables.tf | 1 + 4 files changed, 47 insertions(+) create mode 120000 examples/resources/netapp-ontap_security_certificate/provider.tf create mode 100644 examples/resources/netapp-ontap_security_certificate/resource.tf create mode 120000 examples/resources/netapp-ontap_security_certificate/terraform.tfvars create mode 120000 examples/resources/netapp-ontap_security_certificate/variables.tf diff --git a/examples/resources/netapp-ontap_security_certificate/provider.tf b/examples/resources/netapp-ontap_security_certificate/provider.tf new file mode 120000 index 00000000..c6b7138f --- /dev/null +++ b/examples/resources/netapp-ontap_security_certificate/provider.tf @@ -0,0 +1 @@ +../../provider/provider.tf \ No newline at end of file diff --git a/examples/resources/netapp-ontap_security_certificate/resource.tf b/examples/resources/netapp-ontap_security_certificate/resource.tf new file mode 100644 index 00000000..d5b8f655 --- /dev/null +++ b/examples/resources/netapp-ontap_security_certificate/resource.tf @@ -0,0 +1,44 @@ +# creating a certificate +resource "netapp-ontap_security_certificate" "create_certificate" { + cx_profile_name = "cluster5" + name = "svm2_ca_cert1_unique" + common_name = "svm2_ca_cert1" + type = "root_ca" + svm_name = "svm2" + expiry_time = "P365DT" +} + +# signing a certificate +resource "netapp-ontap_security_certificate" "sign_certificate" { + cx_profile_name = "cluster5" + name = "svm2_ca_cert1_unique" + common_name = "svm2_ca_cert1" + type = "root_ca" + svm_name = "svm3" + expiry_time = "P90DT" + signing_request = <<-EOT +-----BEGIN CERTIFICATE REQUEST----- +signing-request +-----END CERTIFICATE REQUEST----- +EOT +} + +# installing a certificate +resource "netapp-ontap_security_certificate" "install_certificate" { + cx_profile_name = "cluster5" + common_name = "svm3_cert1" + type = "server" + svm_name = "svm3" + expiry_time = "P90DT" + public_certificate = <<-EOT +-----BEGIN CERTIFICATE----- +certificate +-----END CERTIFICATE----- +EOT + + private_key = <<-EOT +-----BEGIN PRIVATE KEY----- +private-key +-----END PRIVATE KEY----- +EOT +} diff --git a/examples/resources/netapp-ontap_security_certificate/terraform.tfvars b/examples/resources/netapp-ontap_security_certificate/terraform.tfvars new file mode 120000 index 00000000..8d9d1c96 --- /dev/null +++ b/examples/resources/netapp-ontap_security_certificate/terraform.tfvars @@ -0,0 +1 @@ +../../provider/terraform.tfvars \ No newline at end of file diff --git a/examples/resources/netapp-ontap_security_certificate/variables.tf b/examples/resources/netapp-ontap_security_certificate/variables.tf new file mode 120000 index 00000000..395ce618 --- /dev/null +++ b/examples/resources/netapp-ontap_security_certificate/variables.tf @@ -0,0 +1 @@ +../../provider/variables.tf \ No newline at end of file From 7768147a3ba72535b3beb9e2b02ab9fc4adf9052 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 12:35:45 +0530 Subject: [PATCH 04/18] updated unmarshalResponse --- internal/restclient/rest_response.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/restclient/rest_response.go b/internal/restclient/rest_response.go index 2bb64ac3..9a58286c 100644 --- a/internal/restclient/rest_response.go +++ b/internal/restclient/rest_response.go @@ -188,13 +188,19 @@ func (c *RestClient) unmarshalResponse(statusCode int, responseJSON []byte, http // If Other is present, add it to records. // But ignore it if we already have some records. - // Other will always have 1 element called _link, so only do this if Other has more than 1 element + // Add the records under Other if Other has more than 1 element + // and if Other has only one element and it is not _links // Examples: // {NumRecords:0 Records:[] Error:{Code: Message: Target:} Job:map[] Jobs:[] Other:map[_links:map[self:map[href:/api/cluster/schedules?fields=name%2Cuuid%2Ccron%2Cinterval%2Ctype%2Cscope&name=mytest]]]} // {NumRecords:0 Records:[] Error:{Code: Message: Target:} Job:map[] Jobs:[] Other:map[_links:map[self:map[href:/api/cluster]] certificate:map[_links:map[self:map[href:/api/security/certificates/2f632ea7-92cd-11ed-8f2b-005056b3357c]] uuid:2f632ea7-92cd-11ed-8f2b-005056b3357c] metric:map[duration:PT15S iops:map[other:0 read:0 total:0 write:0] latency:map[other:0 read:0 total:0 write:0] status:ok throughput:map[other:0 read:0 total:0 write:0] timestamp:2023-03-16T18:36:30Z] name:laurentncluster-2 peering_policy:map[authentication_required:true encryption_required:false minimum_passphrase_length:8] san_optimized:false statistics:map[iops_raw:map[other:0 read:0 total:0 write:0] latency_raw:map[other:0 read:0 total:0 write:0] status:ok throughput_raw:map[other:0 read:0 total:0 write:0] timestamp:2023-03-16T18:36:31Z] timezone:map[name:Etc/UTC] uuid:2115008a-92cd-11ed-8f2b-005056b3357c version:map[full:NetApp Release Metropolitan__9.11.1: Sat Dec 10 19:08:07 UTC 2022 generation:9 major:11 minor:1]]} - if rawResponse.NumRecords == 0 && len(rawResponse.Records) == 0 && len(rawResponse.Other) > 1 { - rawResponse.NumRecords = 1 - rawResponse.Records = append(rawResponse.Records, rawResponse.Other) + // {NumRecords:0 Records:[] Error:{Code: Message: Target:} Job:map[] Jobs:[] Other:map[public_certificate:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n]} + // if rawResponse.NumRecords == 0 && len(rawResponse.Records) == 0 && len(rawResponse.Other) > 1 { + if rawResponse.NumRecords == 0 && len(rawResponse.Records) == 0 { + _, found := rawResponse.Other["_links"] + if (len(rawResponse.Other) == 1 && found == false) || len(rawResponse.Other) > 1 { + rawResponse.NumRecords = 1 + rawResponse.Records = append(rawResponse.Records, rawResponse.Other) + } } var finalResponse RestResponse From e6632ffbeb1fc96379ee101726ae16421361e401 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 16:21:30 +0530 Subject: [PATCH 05/18] added connection profile --- examples/provider/provider.tf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 3e684d4a..354f521f 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -40,6 +40,13 @@ provider "netapp-ontap" { password = var.password validate_certs = var.validate_certs }, + { + name = "cluster5" + hostname = "********2" + username = var.username + password = var.password + validate_certs = var.validate_certs + }, { name = "clustercifs" hostname = "********189" From 4dda71c911fb5cbaa3c0ca63f1b77317900d3eb1 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 16:21:57 +0530 Subject: [PATCH 06/18] updated examples --- .../resource.tf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/resources/netapp-ontap_security_certificate/resource.tf b/examples/resources/netapp-ontap_security_certificate/resource.tf index d5b8f655..5aa107dc 100644 --- a/examples/resources/netapp-ontap_security_certificate/resource.tf +++ b/examples/resources/netapp-ontap_security_certificate/resource.tf @@ -1,20 +1,20 @@ # creating a certificate resource "netapp-ontap_security_certificate" "create_certificate" { cx_profile_name = "cluster5" - name = "svm2_ca_cert1_unique" - common_name = "svm2_ca_cert1" + name = "tfsvm_ca_cert1" + common_name = "tfsvm_ca_cert" type = "root_ca" - svm_name = "svm2" + svm_name = "tfsvm" expiry_time = "P365DT" } # signing a certificate resource "netapp-ontap_security_certificate" "sign_certificate" { cx_profile_name = "cluster5" - name = "svm2_ca_cert1_unique" - common_name = "svm2_ca_cert1" + name = "tfsvm_ca_cert1" + common_name = "tfsvm_ca_cert" type = "root_ca" - svm_name = "svm3" + svm_name = "svm1" expiry_time = "P90DT" signing_request = <<-EOT -----BEGIN CERTIFICATE REQUEST----- @@ -26,9 +26,9 @@ EOT # installing a certificate resource "netapp-ontap_security_certificate" "install_certificate" { cx_profile_name = "cluster5" - common_name = "svm3_cert1" + common_name = "svm1_cert1" type = "server" - svm_name = "svm3" + svm_name = "svm1" expiry_time = "P90DT" public_certificate = <<-EOT -----BEGIN CERTIFICATE----- From 1c4d7b0ab76d0e54464c6855f654aadc003ed443 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Fri, 4 Oct 2024 16:22:52 +0530 Subject: [PATCH 07/18] fix go lint issues --- internal/provider/security/security_certificate_resource.go | 2 +- internal/restclient/rest_response.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/provider/security/security_certificate_resource.go b/internal/provider/security/security_certificate_resource.go index 9f6a20df..8c596e4b 100644 --- a/internal/provider/security/security_certificate_resource.go +++ b/internal/provider/security/security_certificate_resource.go @@ -371,7 +371,7 @@ func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.C if cluster.Version.Generation == 9 && cluster.Version.Major >= 8 { body.Name = data.Name.ValueString() } else { - tflog.Error(ctx, fmt.Sprintf("'name' is supported with ONTAP 9.8 or higher.")) + tflog.Error(ctx, "'name' is supported with ONTAP 9.8 or higher.") } } body.CommonName = data.CommonName.ValueString() diff --git a/internal/restclient/rest_response.go b/internal/restclient/rest_response.go index 9a58286c..56d646a1 100644 --- a/internal/restclient/rest_response.go +++ b/internal/restclient/rest_response.go @@ -197,7 +197,7 @@ func (c *RestClient) unmarshalResponse(statusCode int, responseJSON []byte, http // if rawResponse.NumRecords == 0 && len(rawResponse.Records) == 0 && len(rawResponse.Other) > 1 { if rawResponse.NumRecords == 0 && len(rawResponse.Records) == 0 { _, found := rawResponse.Other["_links"] - if (len(rawResponse.Other) == 1 && found == false) || len(rawResponse.Other) > 1 { + if (len(rawResponse.Other) == 1 && !found) || len(rawResponse.Other) > 1 { rawResponse.NumRecords = 1 rawResponse.Records = append(rawResponse.Records, rawResponse.Other) } From 8078ee01cbfdd7202babbb5cf587e31ccd6dd31c Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Mon, 7 Oct 2024 12:29:25 +0530 Subject: [PATCH 08/18] update acc tests --- .../security_certificate_resource_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/provider/security/security_certificate_resource_test.go b/internal/provider/security/security_certificate_resource_test.go index 675b471a..57e4519e 100644 --- a/internal/provider/security/security_certificate_resource_test.go +++ b/internal/provider/security/security_certificate_resource_test.go @@ -19,16 +19,16 @@ func TestAccSecurityCertificateResource(t *testing.T) { { Config: testAccSecurityCertificateResourceCertificateConfig(), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "tfsvm_ca_cert1"), + resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "acc_test_ca_cert2"), ), }, // Import and read { ResourceName: "netapp-ontap_security_certificate.example", ImportState: true, - ImportStateId: fmt.Sprintf("%s,%s", "tfsvm_ca_cert1", "cluster1"), + ImportStateId: fmt.Sprintf("%s,%s", "acc_test_ca_cert1", "cluster1"), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "tfsvm_ca_cert1"), + resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "acc_test_ca_cert1"), ), }, // Delete testing automatically occurs in TestCase @@ -37,11 +37,11 @@ func TestAccSecurityCertificateResource(t *testing.T) { } func testAccSecurityCertificateResourceCertificateConfig() string { - host := os.Getenv("TF_ACC_NETAPP_HOST") + host := os.Getenv("TF_ACC_NETAPP_HOST_CIFS") admin := os.Getenv("TF_ACC_NETAPP_USER") - password := os.Getenv("TF_ACC_NETAPP_PASS") + password := os.Getenv("TF_ACC_NETAPP_PASS2") if host == "" || admin == "" || password == "" { - fmt.Println("TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests") + fmt.Println("TF_ACC_NETAPP_HOST_CIFS, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS2 must be set for acceptance tests") os.Exit(1) } return fmt.Sprintf(` @@ -59,9 +59,9 @@ provider "netapp-ontap" { resource "netapp-ontap_security_certificate" "example" { cx_profile_name = "cluster1" - name = "tfsvm_ca_cert1" - common_name = "tfsvm_ca_cert" + name = "acc_test_ca_cert2" + common_name = "acc_test_ca_cert" type = "root_ca" - svm_name = "tfsvm" + svm_name = "acc_test" }`, host, admin, password) } From b168ca886e4ed405d70083042a14cdfb5cbc0c68 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Mon, 7 Oct 2024 15:04:54 +0530 Subject: [PATCH 09/18] update examples --- .../resources/netapp-ontap_security_certificate/resource.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/resources/netapp-ontap_security_certificate/resource.tf b/examples/resources/netapp-ontap_security_certificate/resource.tf index 5aa107dc..1a18e347 100644 --- a/examples/resources/netapp-ontap_security_certificate/resource.tf +++ b/examples/resources/netapp-ontap_security_certificate/resource.tf @@ -14,7 +14,7 @@ resource "netapp-ontap_security_certificate" "sign_certificate" { name = "tfsvm_ca_cert1" common_name = "tfsvm_ca_cert" type = "root_ca" - svm_name = "svm1" + svm_name = "svm1" # SVM on which the signed certificate will exist expiry_time = "P90DT" signing_request = <<-EOT -----BEGIN CERTIFICATE REQUEST----- From 173f8270cd89c379e27bf66916b85b5acf80599f Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Mon, 7 Oct 2024 15:06:04 +0530 Subject: [PATCH 10/18] new resource security_certificate --- docs/resources/security_certificate.md | 147 +++++++++++++++++++++++++ scripts/generate_docs.py | 1 + 2 files changed, 148 insertions(+) create mode 100644 docs/resources/security_certificate.md diff --git a/docs/resources/security_certificate.md b/docs/resources/security_certificate.md new file mode 100644 index 00000000..13267fc3 --- /dev/null +++ b/docs/resources/security_certificate.md @@ -0,0 +1,147 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "netapp-ontap_security_certificate Resource - terraform-provider-netapp-ontap" +subcategory: "" +description: |- + SecurityCertificate resource +--- + +# netapp-ontap_security_certificate (Resource) + +Create/ install/ sign a certificate + +### Related ONTAP commands +```commandline +* security certificate show +* security certificate create +* security certificate install +* security certificate sign +``` + +## Supported Platforms +* On-prem ONTAP system 9.6 or higher + +## Example Usage + +```terraform +# creating a certificate +resource "netapp-ontap_security_certificate" "create_certificate" { + cx_profile_name = "cluster5" + name = "tfsvm_ca_cert1" + common_name = "tfsvm_ca_cert" + type = "root_ca" + svm_name = "tfsvm" + expiry_time = "P365DT" +} + +# signing a certificate +resource "netapp-ontap_security_certificate" "sign_certificate" { + cx_profile_name = "cluster5" + name = "tfsvm_ca_cert1" + common_name = "tfsvm_ca_cert" + type = "root_ca" + svm_name = "svm1" # SVM on which the signed certificate will exist + expiry_time = "P90DT" + signing_request = <<-EOT +-----BEGIN CERTIFICATE REQUEST----- +signing-request +-----END CERTIFICATE REQUEST----- +EOT +} + +# installing a certificate +resource "netapp-ontap_security_certificate" "install_certificate" { + cx_profile_name = "cluster5" + common_name = "svm1_cert1" + type = "server" + svm_name = "svm1" + expiry_time = "P90DT" + public_certificate = <<-EOT +-----BEGIN CERTIFICATE----- +certificate +-----END CERTIFICATE----- +EOT + + private_key = <<-EOT +-----BEGIN PRIVATE KEY----- +private-key +-----END PRIVATE KEY----- +EOT +} +``` + + +## Schema + +### Required + +- `cx_profile_name` (String) Connection profile name. +- `common_name` (String) Common name of the certificate. +- `type` (String) Type of certificate. + +### Optional + +- `expiry_time` (String) Certificate expiration time, in ISO 8601 duration format or date and time format. +- `hash_function` (String) Hashing function. +- `key_size` (Number) Key size of the certificate in bits. +- `name` (String) The unique name of the security certificate per SVM. +- `private_key` (String, Sensitive) Private key Certificate in PEM format. Only valid when installing a CA-signed certificate. +- `public_certificate` (String) Public key Certificate in PEM format. If this is not provided during create action, a self-signed certificate is created. +- `signing_request` (String) Certificate signing request to be signed by the given certificate authority. Request should be in X509 PEM format. +- `svm_name` (String) Name of the SVM in which the certificate is created or installed or the SVM on which the signed certificate will exist. + +### Read-Only + +- `ca` (String) Certificate authority. +- `id` (String) UUID of the certificate. +- `scope` (String) Set to 'svm' for certificates installed in a SVM. Otherwise, set to 'cluster'. +- `serial_number` (String) Serial number of the certificate. +- `signed_certificate` (String) Signed public key Certificate in PEM format that is returned while signing a certificate. + +## Import +This resource supports import, which allows you to import existing security certificate into the state of this resource. +Import require a unique ID composed of the security certificate name and connection profile, separated by a comma. + +id = `name`, `cx_profile_name` + +### Terraform Import + + For example + ```shell + terraform import netapp-ontap_security_certificate.cert_import tfsvm_ca_cert1,cluster5 + ``` + +### Terraform Import Block +This requires Terraform 1.5 or higher, and will auto create the configuration for you + +First create the block +```terraform +import { + to = netapp-ontap_security_certificate.cert_import + id = "tfsvm_ca_cert1,cluster5" +} +``` +Next run, this will auto create the configuration for you +```shell +terraform plan -generate-config-out=generated.tf +``` +This will generate a file called generated.tf, which will contain the configuration for the imported resource +```terraform +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. + +# __generated__ by Terraform from "tfsvm_ca_cert1,cluster5" +resource "netapp-ontap_security_certificate" "cert_import" { + common_name = "tfsvm_ca_cert" + cx_profile_name = "cluster5" + expiry_time = "2025-10-04T01:24:54-04:00" + hash_function = "sha256" + key_size = 2048 + name = "tfsvm_ca_cert1" + private_key = null # sensitive + public_certificate = "-----BEGIN CERTIFICATE-----\ncertificate\n-----END CERTIFICATE-----\n" + signing_request = null + svm_name = "tfsvm" + type = "root_ca" +} +``` \ No newline at end of file diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py index 32e36f30..29879ac7 100755 --- a/scripts/generate_docs.py +++ b/scripts/generate_docs.py @@ -83,6 +83,7 @@ "security_roles_data_source.md", "security_roles_resource.md", "security_login_message_resource.md", + "security_certificate_resource.md", ], 'snaplock': [], 'snapmirror': [ From 9a30df65b4c4458137257b457b9aade164172d4e Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Tue, 8 Oct 2024 15:43:37 +0530 Subject: [PATCH 11/18] added error reporting --- .../security/security_certificate_resource.go | 100 +++++++++--------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/internal/provider/security/security_certificate_resource.go b/internal/provider/security/security_certificate_resource.go index 8c596e4b..245a4e7b 100644 --- a/internal/provider/security/security_certificate_resource.go +++ b/internal/provider/security/security_certificate_resource.go @@ -281,30 +281,41 @@ func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.C // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.SigningRequest.IsNull() { - // this if block is for signing security certificate - var body interfaces.SecurityCertificateResourceSignBodyDataModelONTAP - errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) - - if resp.Diagnostics.HasError() { - return - } + if resp.Diagnostics.HasError() { + return + } + + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } - client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) - if err != nil { - // error reporting done inside NewClient + cluster, err := interfaces.GetCluster(errorHandler, *client) + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "Cluster not found.") + return + } + if err != nil { + // error reporting done inside GetCluster + return + } + + name_supported := false + if !data.Name.IsNull() && data.Name.ValueString() != "" { + if cluster.Version.Generation == 9 && cluster.Version.Major < 8 { + tflog.Error(ctx, "'name' is supported with ONTAP 9.8 or higher.") + errorHandler.MakeAndReportError("Unsupported parameter", "'name' is supported with ONTAP 9.8 or higher.") return + } else { + name_supported = true } + } - cluster, err := interfaces.GetCluster(errorHandler, *client) - if cluster == nil { - errorHandler.MakeAndReportError("No cluster found", "Cluster not found.") - return - } - if err != nil { - // error reporting done inside GetCluster - return - } + if !data.SigningRequest.IsNull() { + // this if block is for signing security certificate + var body interfaces.SecurityCertificateResourceSignBodyDataModelONTAP // Read the updated data from the API restInfo, err := interfaces.GetSecurityCertificate(errorHandler, *client, cluster.Version, data.Name.ValueString(), data.CommonName.ValueString(), data.Type.ValueString()) @@ -345,33 +356,10 @@ func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.C } else { // This else block is for creating or installing security certificate var body interfaces.SecurityCertificateResourceCreateBodyDataModelONTAP - errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) - - if resp.Diagnostics.HasError() { - return - } - client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) - if err != nil { - // error reporting done inside NewClient - return - } - - cluster, err := interfaces.GetCluster(errorHandler, *client) - if cluster == nil { - errorHandler.MakeAndReportError("No cluster found", "Cluster not found.") - return - } - if err != nil { - // error reporting done inside GetCluster - return - } - - if !data.Name.IsUnknown() { - if cluster.Version.Generation == 9 && cluster.Version.Major >= 8 { + if !data.Name.IsNull() && data.Name.ValueString() != "" { + if name_supported { body.Name = data.Name.ValueString() - } else { - tflog.Error(ctx, "'name' is supported with ONTAP 9.8 or higher.") } } body.CommonName = data.CommonName.ValueString() @@ -396,8 +384,8 @@ func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.C } var operation string - if !data.PublicCertificate.IsUnknown() || !data.PrivateKey.IsUnknown() { - operation = "signing" + if !data.PublicCertificate.IsUnknown() || !data.PrivateKey.IsNull() { + operation = "installing" } else { operation = "creating" } @@ -492,15 +480,25 @@ func (r *SecurityCertificateResource) ImportState(ctx context.Context, req resou // Parse the ID idParts := strings.Split(req.ID, ",") - // import name and cx_profile - if len(idParts) == 2 { + // import name, common_name, type and cx_profile + if len(idParts) == 4 { resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("common_name"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("type"), idParts[2])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[3])...) + return + } + + // import common_name, type, and cx_profile + if len(idParts) == 3 { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("common_name"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("type"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[2])...) return } resp.Diagnostics.AddError( "Unexpected Import Identifier", - fmt.Sprintf("Expected import identifier with format: name,cx_profile_name. Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: name,common_name,type,cx_profile_name or common_name,type,cx_profile_name. Got: %q", req.ID), ) } From 1388db58e2b87abd1cbcf7dfecf5eb5aa3b39b6e Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Tue, 8 Oct 2024 15:43:57 +0530 Subject: [PATCH 12/18] update acc tests --- .../provider/security/security_certificate_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/security/security_certificate_resource_test.go b/internal/provider/security/security_certificate_resource_test.go index 57e4519e..dd022955 100644 --- a/internal/provider/security/security_certificate_resource_test.go +++ b/internal/provider/security/security_certificate_resource_test.go @@ -26,7 +26,7 @@ func TestAccSecurityCertificateResource(t *testing.T) { { ResourceName: "netapp-ontap_security_certificate.example", ImportState: true, - ImportStateId: fmt.Sprintf("%s,%s", "acc_test_ca_cert1", "cluster1"), + ImportStateId: fmt.Sprintf("%s,%s,%s,%s", "acc_test_ca_cert1", "acc_test_ca_cert", "root_ca", "cluster1"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netapp-ontap_security_certificate.example", "name", "acc_test_ca_cert1"), ), From 6165e9f426aba18e8115dc6298822ef835b6162f Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Tue, 8 Oct 2024 15:44:34 +0530 Subject: [PATCH 13/18] update doc --- docs/resources/security_certificate.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/resources/security_certificate.md b/docs/resources/security_certificate.md index 13267fc3..5a287f2f 100644 --- a/docs/resources/security_certificate.md +++ b/docs/resources/security_certificate.md @@ -100,15 +100,22 @@ EOT ## Import This resource supports import, which allows you to import existing security certificate into the state of this resource. -Import require a unique ID composed of the security certificate name and connection profile, separated by a comma. +Import require a unique ID composed of the security certificate name, common name, type and connection profile, separated by a comma or security certificate common name, type, and connection profile, separated by a comma. -id = `name`, `cx_profile_name` +id = `name`,`common_name`,`type`,`cx_profile_name` ### Terraform Import For example + + Import with certificate name; recommended for ONTAP 9.8 or later + ```shell + terraform import netapp-ontap_security_certificate.cert_import tfsvm_ca_cert1,tfsvm_ca_cert,root_ca,cluster5 + ``` + + Import with certificate common name & type; applicable for ONTAP 9.6 or 9.7 ```shell - terraform import netapp-ontap_security_certificate.cert_import tfsvm_ca_cert1,cluster5 + terraform import netapp-ontap_security_certificate.cert_import svm1_cert1,server,cluster5 ``` ### Terraform Import Block @@ -118,7 +125,7 @@ First create the block ```terraform import { to = netapp-ontap_security_certificate.cert_import - id = "tfsvm_ca_cert1,cluster5" + id = "tfsvm_ca_cert1,tfsvm_ca_cert,root_ca,cluster5" } ``` Next run, this will auto create the configuration for you @@ -130,7 +137,7 @@ This will generate a file called generated.tf, which will contain the configurat # __generated__ by Terraform # Please review these resources and move them into your main configuration files. -# __generated__ by Terraform from "tfsvm_ca_cert1,cluster5" +# __generated__ by Terraform from "tfsvm_ca_cert1,tfsvm_ca_cert,root_ca,cluster5" resource "netapp-ontap_security_certificate" "cert_import" { common_name = "tfsvm_ca_cert" cx_profile_name = "cluster5" From 29761e2e0f16ebfdb3a265cc7696a5652545f0fd Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Thu, 10 Oct 2024 12:36:56 +0530 Subject: [PATCH 14/18] update examples --- .../netapp-ontap_security_certificate/resource.tf | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/resources/netapp-ontap_security_certificate/resource.tf b/examples/resources/netapp-ontap_security_certificate/resource.tf index 1a18e347..36eb8cf5 100644 --- a/examples/resources/netapp-ontap_security_certificate/resource.tf +++ b/examples/resources/netapp-ontap_security_certificate/resource.tf @@ -1,3 +1,12 @@ +# creating a cluster-scoped certificate +resource "netapp-ontap_security_certificate" "create_certificate" { + cx_profile_name = "cluster5" + name = "test_ca_cert1" + common_name = "test_ca_cert" + type = "root_ca" + expiry_time = "P365DT" +} + # creating a certificate resource "netapp-ontap_security_certificate" "create_certificate" { cx_profile_name = "cluster5" From 6bedac33d57046b3058aeba6515f8954823fd757 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Thu, 10 Oct 2024 12:37:41 +0530 Subject: [PATCH 15/18] update resource schema --- .../security/security_certificate_resource.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/provider/security/security_certificate_resource.go b/internal/provider/security/security_certificate_resource.go index 245a4e7b..1c901b60 100644 --- a/internal/provider/security/security_certificate_resource.go +++ b/internal/provider/security/security_certificate_resource.go @@ -95,6 +95,7 @@ func (r *SecurityCertificateResource) Schema(ctx context.Context, req resource.S "svm_name": schema.StringAttribute{ MarkdownDescription: "Name of the SVM in which the certificate is created or installed or the SVM on which the signed certificate will exist.", Optional: true, + Computed: true, }, "scope": schema.StringAttribute{ MarkdownDescription: "Set to 'svm' for certificates installed in a SVM. Otherwise, set to 'cluster'.", @@ -132,6 +133,9 @@ func (r *SecurityCertificateResource) Schema(ctx context.Context, req resource.S MarkdownDescription: "Signed public key Certificate in PEM format that is returned while signing a certificate.", Optional: false, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "private_key": schema.StringAttribute{ MarkdownDescription: "Private key Certificate in PEM format. Only valid when installing a CA-signed certificate.", @@ -262,7 +266,7 @@ func (r *SecurityCertificateResource) Read(ctx context.Context, req resource.Rea if data.ExpiryTime.IsNull() { data.ExpiryTime = types.StringValue(restInfo.ExpiryTime) } - if data.SVMName.IsNull() { + if data.SVMName.IsUnknown() { data.SVMName = types.StringValue(restInfo.SVM.Name) } @@ -303,7 +307,7 @@ func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.C } name_supported := false - if !data.Name.IsNull() && data.Name.ValueString() != "" { + if !data.Name.IsNull() && data.Name.ValueString() != "" { if cluster.Version.Generation == 9 && cluster.Version.Major < 8 { tflog.Error(ctx, "'name' is supported with ONTAP 9.8 or higher.") errorHandler.MakeAndReportError("Unsupported parameter", "'name' is supported with ONTAP 9.8 or higher.") @@ -334,6 +338,9 @@ func (r *SecurityCertificateResource) Create(ctx context.Context, req resource.C data.PublicCertificate = types.StringValue(restInfo.PublicCertificate) data.HashFunction = types.StringValue(restInfo.HashFunction) data.KeySize = types.Int64Value(restInfo.KeySize) + if data.SVMName.IsUnknown() { + data.SVMName = types.StringValue(restInfo.SVM.Name) + } body.SigningRequest = data.SigningRequest.ValueString() if !data.HashFunction.IsUnknown() { From c5f4b6845614d3867f236e5e2186322a21849fa5 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Thu, 10 Oct 2024 16:02:32 +0530 Subject: [PATCH 16/18] update examples --- .../resources/netapp-ontap_security_certificate/resource.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/resources/netapp-ontap_security_certificate/resource.tf b/examples/resources/netapp-ontap_security_certificate/resource.tf index 36eb8cf5..f0a3bbdf 100644 --- a/examples/resources/netapp-ontap_security_certificate/resource.tf +++ b/examples/resources/netapp-ontap_security_certificate/resource.tf @@ -1,5 +1,5 @@ # creating a cluster-scoped certificate -resource "netapp-ontap_security_certificate" "create_certificate" { +resource "netapp-ontap_security_certificate" "create_certificate1" { cx_profile_name = "cluster5" name = "test_ca_cert1" common_name = "test_ca_cert" @@ -8,7 +8,7 @@ resource "netapp-ontap_security_certificate" "create_certificate" { } # creating a certificate -resource "netapp-ontap_security_certificate" "create_certificate" { +resource "netapp-ontap_security_certificate" "create_certificate2" { cx_profile_name = "cluster5" name = "tfsvm_ca_cert1" common_name = "tfsvm_ca_cert" From 2bc9b2b74e2cc6e13d7ca551ad895e84ec59cabc Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Thu, 10 Oct 2024 16:02:58 +0530 Subject: [PATCH 17/18] update docs --- docs/resources/security_certificate.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/resources/security_certificate.md b/docs/resources/security_certificate.md index 5a287f2f..d6b6c8fe 100644 --- a/docs/resources/security_certificate.md +++ b/docs/resources/security_certificate.md @@ -24,8 +24,17 @@ Create/ install/ sign a certificate ## Example Usage ```terraform +# creating a cluster-scoped certificate +resource "netapp-ontap_security_certificate" "create_certificate1" { + cx_profile_name = "cluster5" + name = "test_ca_cert1" + common_name = "test_ca_cert" + type = "root_ca" + expiry_time = "P365DT" +} + # creating a certificate -resource "netapp-ontap_security_certificate" "create_certificate" { +resource "netapp-ontap_security_certificate" "create_certificate2" { cx_profile_name = "cluster5" name = "tfsvm_ca_cert1" common_name = "tfsvm_ca_cert" From 7b7e77db0fa140aa0ee1cb0ea1a23c262a211cf1 Mon Sep 17 00:00:00 2001 From: Chandrakanta Sahu Date: Tue, 15 Oct 2024 11:54:22 +0530 Subject: [PATCH 18/18] resolve conflicts --- internal/interfaces/security_certificate.go | 250 ++++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/internal/interfaces/security_certificate.go b/internal/interfaces/security_certificate.go index e69de29b..d4970474 100644 --- a/internal/interfaces/security_certificate.go +++ b/internal/interfaces/security_certificate.go @@ -0,0 +1,250 @@ +package interfaces + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/mitchellh/mapstructure" + "github.com/netapp/terraform-provider-netapp-ontap/internal/restclient" + "github.com/netapp/terraform-provider-netapp-ontap/internal/utils" +) + +// SecurityCertificateGetDataModelONTAP describes the GET record data model using go types for mapping. +type SecurityCertificateGetDataModelONTAP struct { + Name string `mapstructure:"name"` + UUID string `mapstructure:"uuid"` + CommonName string `mapstructure:"common_name"` + SVM svm `mapstructure:"svm"` + Scope string `mapstructure:"scope"` + Type string `mapstructure:"type"` + SerialNumber string `mapstructure:"serial_number"` + CA string `mapstructure:"ca"` + HashFunction string `mapstructure:"hash_function"` + KeySize int64 `mapstructure:"key_size"` + ExpiryTime string `mapstructure:"expiry_time"` + PublicCertificate string `mapstructure:"public_certificate"` +} + +// SignedSecurityCertificateGetDataModelONTAP describes the GET record data model using go types for mapping. +type SignedSecurityCertificateGetDataModelONTAP struct { + SignedCertificate string `mapstructure:"public_certificate"` +} + +// SecurityCertificateResourceCreateBodyDataModelONTAP describes the create/install body data model using go types for mapping. +type SecurityCertificateResourceCreateBodyDataModelONTAP struct { + Name string `mapstructure:"name,omitempty"` + CommonName string `mapstructure:"common_name"` + Type string `mapstructure:"type"` + SVM svm `mapstructure:"svm,omitempty"` + Scope string `mapstructure:"scope,omitempty"` + PublicCertificate string `mapstructure:"public_certificate,omitempty"` + PrivateKey string `mapstructure:"private_key,omitempty"` + HashFunction string `mapstructure:"hash_function,omitempty"` + KeySize int64 `mapstructure:"key_size,omitempty"` + ExpiryTime string `mapstructure:"expiry_time,omitempty"` +} + +// SecurityCertificateResourceSignBodyDataModelONTAP describes the signing body data model using go types for mapping. +type SecurityCertificateResourceSignBodyDataModelONTAP struct { + SigningRequest string `mapstructure:"signing_request"` + HashFunction string `mapstructure:"hash_function,omitempty"` + ExpiryTime string `mapstructure:"expiry_time,omitempty"` +} + +// SecurityCertificateDataSourceFilterModel describes the data source data model for queries. +type SecurityCertificateDataSourceFilterModel struct { + SVMName string `mapstructure:"svm.name"` + Scope string `mapstructure:"scope"` + CommonName string `mapstructure:"common_name"` + Type string `mapstructure:"type"` +} + +// GetSecurityCertificate to get security_certificate info +// Retrieves the certificate with the given name and/or (common name & type) +func GetSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, name string, common_name string, type_ string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + query := r.NewQuery() + if name != "" { + query.Set("name", name) + } else { + query.Set("common_name", common_name) + query.Set("type", type_) + } + var fields = []string{"uuid", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"} + if version.Generation == 9 && version.Major >= 8 { + fields = append(fields, "name") + } + query.Fields(fields) + + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + if strings.Contains(err.Error(), "or more records when only one is expected") { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", "Duplicate records found with the same common_name.") + } + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response, &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificate data source: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// GetSecurityCertificateByName to get security_certificate info +// Retrieves the certificate using its unique name +func GetSecurityCertificateByName(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, name string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + query := r.NewQuery() + if version.Generation == 9 && version.Major >= 8 { + query.Add("name", name) + } else { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", "Attribute 'name' requires ONTAP 9.8 or later.") + } + query.Fields([]string{"uuid", "name", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"}) + + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response, &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificate data source: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// GetSecurityCertificateByUUID to get security_certificate info +// Retrieves the certificate using its UUID +func GetSecurityCertificateByUUID(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, uuid string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates/" + uuid + query := r.NewQuery() + var fields = []string{"uuid", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"} + if version.Generation == 9 && version.Major >= 8 { + fields = append(fields, "name") + } + query.Fields(fields) + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + return nil, errorHandler.MakeAndReportError("error reading security_certificate info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response, &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificate data source: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// GetSecurityCertificates to get security_certificate info for all resources matching a filter +func GetSecurityCertificates(errorHandler *utils.ErrorHandler, r restclient.RestClient, version versionModelONTAP, filter *SecurityCertificateDataSourceFilterModel) ([]SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + query := r.NewQuery() + var fields = []string{"uuid", "common_name", "svm.name", "scope", "type", "serial_number", "ca", "hash_function", "key_size", "expiry_time", "public_certificate"} + if version.Generation == 9 && version.Major >= 8 { + fields = append(fields, "name") + } + query.Fields(fields) + + if filter != nil { + var filterMap map[string]interface{} + if err := mapstructure.Decode(filter, &filterMap); err != nil { + return nil, errorHandler.MakeAndReportError("error encoding security_certificates filter info", fmt.Sprintf("error on filter %#v: %s", filter, err)) + } + query.SetValues(filterMap) + } + + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("security certificates filter: %+v", query)) + statusCode, response, err := r.GetZeroOrMoreRecords(api, query, nil) + if err == nil && response == nil { + err = fmt.Errorf("no response for GET %s", api) + } + if err != nil { + return nil, errorHandler.MakeAndReportError("error reading security_certificates info", fmt.Sprintf("error on GET %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP []SecurityCertificateGetDataModelONTAP + for _, info := range response { + var record SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(info, &record); err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("failed to decode response from GET %s", api), + fmt.Sprintf("error: %s, statusCode %d, info %#v", err, statusCode, info)) + } + dataONTAP = append(dataONTAP, record) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read security_certificates data source: %#v", dataONTAP)) + return dataONTAP, nil +} + +// CreateOrInstallSecurityCertificate to create/ install a security certificate +func CreateOrInstallSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, body SecurityCertificateResourceCreateBodyDataModelONTAP, operation string) (*SecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return nil, errorHandler.MakeAndReportError("error encoding security certificate body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body)) + } + query := r.NewQuery() + query.Add("return_records", "true") + + statusCode, response, err := r.CallCreateMethod(api, query, bodyMap) + if err != nil { + return nil, errorHandler.MakeAndReportError(fmt.Sprintf("error %s security certificate", operation), fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response.Records[0], &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError("error decoding security certificate info", fmt.Sprintf("error on decode storage/security_certificatess info: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Created security certificate: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// SignSecurityCertificate to sign a security_certificate +func SignSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string, body SecurityCertificateResourceSignBodyDataModelONTAP) (*SignedSecurityCertificateGetDataModelONTAP, error) { + api := "security/certificates" + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return nil, errorHandler.MakeAndReportError("error encoding security certificate body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body)) + } + query := r.NewQuery() + query.Add("return_records", "true") + + statusCode, response, err := r.CallCreateMethod(api+"/"+uuid+"/sign", query, bodyMap) + if err != nil { + return nil, errorHandler.MakeAndReportError("error signing security certificate", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + var dataONTAP SignedSecurityCertificateGetDataModelONTAP + if err := mapstructure.Decode(response.Records[0], &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError("error decoding signed security certificate info", fmt.Sprintf("error on decode storage/security_certificatess/{ca.uuid}/sign info: %s, statusCode %d, response %#v", err, statusCode, response)) + } + tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Signed security certificate: %#v", dataONTAP)) + return &dataONTAP, nil +} + +// DeleteSecurityCertificate to delete a security_certificate +func DeleteSecurityCertificate(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string) error { + api := "security/certificates" + statusCode, _, err := r.CallDeleteMethod(api+"/"+uuid, nil, nil) + if err != nil { + return errorHandler.MakeAndReportError("error deleting security certificate", fmt.Sprintf("error on DELETE %s: %s, statusCode %d", api, err, statusCode)) + } + return nil +}