From 14a2bb06bc88ea890c84406375c97f296c1844eb Mon Sep 17 00:00:00 2001 From: Thibaut Di Prima Date: Wed, 20 Nov 2024 16:33:50 +0000 Subject: [PATCH] add region resource volume --- ovh/provider_new.go | 1 + ovh/resource_cloud_project_volume.go | 208 +++++ ovh/resource_cloud_project_volume_gen.go | 795 ++++++++++++++++++ ovh/resource_cloud_project_volume_test.go | 44 + .../r/cloud_project_region_volume.markdown | 44 + 5 files changed, 1092 insertions(+) create mode 100644 ovh/resource_cloud_project_volume.go create mode 100644 ovh/resource_cloud_project_volume_gen.go create mode 100644 ovh/resource_cloud_project_volume_test.go create mode 100644 website/docs/r/cloud_project_region_volume.markdown diff --git a/ovh/provider_new.go b/ovh/provider_new.go index 1d764d1e..f724e147 100644 --- a/ovh/provider_new.go +++ b/ovh/provider_new.go @@ -219,6 +219,7 @@ func (p *OvhProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewCloudProjectAlertingResource, NewCloudProjectGatewayInterfaceResource, + NewCloudProjectVolumeResource, NewDbaasLogsTokenResource, NewDedicatedServerResource, NewDomainZoneDnssecResource, diff --git a/ovh/resource_cloud_project_volume.go b/ovh/resource_cloud_project_volume.go new file mode 100644 index 00000000..66ae8340 --- /dev/null +++ b/ovh/resource_cloud_project_volume.go @@ -0,0 +1,208 @@ +package ovh + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/ovh/go-ovh/ovh" +) + +var _ resource.ResourceWithConfigure = (*cloudProjectVolumeResource)(nil) + +func NewCloudProjectVolumeResource() resource.Resource { + return &cloudProjectVolumeResource{} +} + +type cloudProjectVolumeResource struct { + config *Config +} + +func (r *cloudProjectVolumeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_cloud_project_volume" +} + +func (d *cloudProjectVolumeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*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), + ) + return + } + + d.config = config +} + +func (d *cloudProjectVolumeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = CloudProjectVolumeResourceSchema(ctx) +} + +func (r *cloudProjectVolumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data, responseData CloudProjectVolumeModelOp + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + endpoint := "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/region/" + url.PathEscape(data.RegionName.ValueString()) + "/volume" + if err := r.config.OVHClient.Post(endpoint, data.ToCreate(), &responseData); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Post %s", endpoint), + err.Error(), + ) + return + } + + err := r.WaitForVolumeCreation(ctx, r.config.OVHClient, data.ServiceName.ValueString(), responseData.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Operation"), + err.Error(), + ) + return + } + + res := &CloudProjectVolumeModelOp{} + endpoint = "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/operation/" + url.PathEscape(responseData.Id.ValueString()) + if err := r.config.OVHClient.Get(endpoint, res); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Post %s", endpoint), + err.Error(), + ) + return + } + + resVol := &CloudProjectVolumeModelOp{} + endpoint = "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/region/" + url.PathEscape(data.RegionName.ValueString()) + "/volume/" + url.PathEscape(res.ResourceId.ValueString()) + if err := r.config.OVHClient.Get(endpoint, resVol); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Post %s", endpoint), + err.Error(), + ) + return + } + + data.MergeWith(resVol) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *cloudProjectVolumeResource) WaitForVolumeCreation(ctx context.Context, client *ovh.Client, serviceName, operationId string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{"null", "in-progress", "created", ""}, + Target: []string{"completed"}, + Refresh: func() (interface{}, string, error) { + res := &CloudProjectVolumeModelOp{} + endpoint := "/cloud/project/" + url.PathEscape(serviceName) + "/operation/" + url.PathEscape(operationId) + err := client.GetWithContext(ctx, endpoint, res) + if err != nil { + return res, "", err + } + return res, res.Status.ValueString(), nil + }, + Timeout: 360 * time.Second, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + return err +} + +func (r *cloudProjectVolumeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data, responseData CloudProjectVolumeModelOp + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + endpoint := "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/region/" + url.PathEscape(data.RegionName.ValueString()) + "/volume/" + url.PathEscape(data.VolumeId.ValueString()) + "" + + if err := r.config.OVHClient.Get(endpoint, &responseData); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Get %s", endpoint), + err.Error(), + ) + return + } + + data.MergeWith(&responseData) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *cloudProjectVolumeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, planData, responseData CloudProjectVolumeModelOp + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Update resource + endpoint := "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/region/" + url.PathEscape(data.RegionName.ValueString()) + "/volume" + if err := r.config.OVHClient.Post(endpoint, planData.ToUpdate(), nil); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Post %s", endpoint), + err.Error(), + ) + return + } + + // Read updated resource + endpoint = "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/region/" + url.PathEscape(data.RegionName.ValueString()) + "/volume/" + url.PathEscape(data.VolumeId.ValueString()) + "" + if err := r.config.OVHClient.Get(endpoint, &responseData); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Get %s", endpoint), + err.Error(), + ) + return + } + + responseData.MergeWith(&planData) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &responseData)...) +} + +func (r *cloudProjectVolumeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data CloudProjectVolumeModelOp + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Delete API call logic + endpoint := "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + "/volume/" + url.PathEscape(data.VolumeId.ValueString()) + "" + if err := r.config.OVHClient.Delete(endpoint, nil); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error calling Delete %s", endpoint), + err.Error(), + ) + } +} diff --git a/ovh/resource_cloud_project_volume_gen.go b/ovh/resource_cloud_project_volume_gen.go new file mode 100644 index 00000000..2b6d1168 --- /dev/null +++ b/ovh/resource_cloud_project_volume_gen.go @@ -0,0 +1,795 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package ovh + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + ovhtypes "github.com/ovh/terraform-provider-ovh/ovh/types" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func CloudProjectVolumeResourceSchema(ctx context.Context) schema.Schema { + attrs := map[string]schema.Attribute{ + "action": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "The action of the operation", + MarkdownDescription: "The action of the operation", + }, + "completed_at": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "The completed date of the operation", + MarkdownDescription: "The completed date of the operation", + }, + "created_at": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "The creation date of the operation", + MarkdownDescription: "The creation date of the operation", + }, + "description": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Volume description", + MarkdownDescription: "Volume description", + }, + "id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "Operation ID", + MarkdownDescription: "Operation ID", + }, + "image_id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Image ID", + MarkdownDescription: "Image ID", + }, + "instance_id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Instance ID", + MarkdownDescription: "Instance ID", + }, + "name": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Volume name", + MarkdownDescription: "Volume name", + }, + "progress": schema.Int64Attribute{ + CustomType: ovhtypes.TfInt64Type{}, + Computed: true, + Description: "Volume status", + MarkdownDescription: "Volume status", + }, + "region_name": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Required: true, + Description: "Region name", + MarkdownDescription: "Region name", + }, + "regions": schema.ListAttribute{ + CustomType: ovhtypes.NewTfListNestedType[ovhtypes.TfStringValue](ctx), + Computed: true, + Description: "List of regions", + MarkdownDescription: "List of regions", + }, + "resource_id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "Id of the resource", + MarkdownDescription: "Id of the resource", + }, + "service_name": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Required: true, + Description: "Service name", + MarkdownDescription: "Service name", + }, + "size": schema.Int64Attribute{ + CustomType: ovhtypes.TfInt64Type{}, + Optional: true, + Computed: true, + Description: "Volume size", + MarkdownDescription: "Volume size", + }, + "snapshot_id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Snapshot ID", + MarkdownDescription: "Snapshot ID", + }, + "started_at": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "Datetime of the operation creation", + MarkdownDescription: "Datetime of the operation creation", + }, + "status": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "Volume status", + MarkdownDescription: "Volume status", + }, + "sub_operations": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "resource_id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "Affected resource of the sub-operation", + MarkdownDescription: "Affected resource of the sub-operation", + }, + "resource_type": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Computed: true, + Description: "The started date of the sub-operation", + MarkdownDescription: "The started date of the sub-operation", + }, + }, + CustomType: SubOperationsType{ + ObjectType: types.ObjectType{ + AttrTypes: SubOperationsValue{}.AttributeTypes(ctx), + }, + }, + }, + CustomType: ovhtypes.NewTfListNestedType[SubOperationsValue](ctx), + Computed: true, + Description: "Sub-operations of the operation", + MarkdownDescription: "Sub-operations of the operation", + }, + "type": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Type of the volume", + MarkdownDescription: "Type of the volume", + Validators: []validator.String{ + stringvalidator.OneOf( + "classic", + "classic-BETA", + "high-speed", + "high-speed-BETA", + "high-speed-gen2", + ), + }, + }, + "volume_id": schema.StringAttribute{ + CustomType: ovhtypes.TfStringType{}, + Optional: true, + Computed: true, + Description: "Volume ID", + MarkdownDescription: "Volume ID", + }, + } + + return schema.Schema{ + Attributes: attrs, + } +} + +type CloudProjectVolumeModelOp struct { + Action ovhtypes.TfStringValue `tfsdk:"action" json:"action"` + CompletedAt ovhtypes.TfStringValue `tfsdk:"completed_at" json:"completedAt"` + CreatedAt ovhtypes.TfStringValue `tfsdk:"created_at" json:"createdAt"` + Description ovhtypes.TfStringValue `tfsdk:"description" json:"description"` + Id ovhtypes.TfStringValue `tfsdk:"id" json:"id"` + ImageId ovhtypes.TfStringValue `tfsdk:"image_id" json:"imageId"` + InstanceId ovhtypes.TfStringValue `tfsdk:"instance_id" json:"instanceId"` + Name ovhtypes.TfStringValue `tfsdk:"name" json:"name"` + Progress ovhtypes.TfInt64Value `tfsdk:"progress" json:"progress"` + RegionName ovhtypes.TfStringValue `tfsdk:"region_name" json:"regionName"` + Regions ovhtypes.TfListNestedValue[ovhtypes.TfStringValue] `tfsdk:"regions" json:"regions"` + ResourceId ovhtypes.TfStringValue `tfsdk:"resource_id" json:"resourceId"` + ServiceName ovhtypes.TfStringValue `tfsdk:"service_name" json:"serviceName"` + Size ovhtypes.TfInt64Value `tfsdk:"size" json:"size"` + SnapshotId ovhtypes.TfStringValue `tfsdk:"snapshot_id" json:"snapshotId"` + StartedAt ovhtypes.TfStringValue `tfsdk:"started_at" json:"startedAt"` + Status ovhtypes.TfStringValue `tfsdk:"status" json:"status"` + SubOperations ovhtypes.TfListNestedValue[SubOperationsValue] `tfsdk:"sub_operations" json:"subOperations"` + Type ovhtypes.TfStringValue `tfsdk:"type" json:"type"` + VolumeId ovhtypes.TfStringValue `tfsdk:"volume_id" json:"volumeId"` +} + +func (v *CloudProjectVolumeModelOp) MergeWith(other *CloudProjectVolumeModelOp) { + + if (v.Action.IsUnknown() || v.Action.IsNull()) && !other.Action.IsUnknown() { + v.Action = other.Action + } + + if (v.CompletedAt.IsUnknown() || v.CompletedAt.IsNull()) && !other.CompletedAt.IsUnknown() { + v.CompletedAt = other.CompletedAt + } + + if (v.CreatedAt.IsUnknown() || v.CreatedAt.IsNull()) && !other.CreatedAt.IsUnknown() { + v.CreatedAt = other.CreatedAt + } + + if (v.Description.IsUnknown() || v.Description.IsNull()) && !other.Description.IsUnknown() { + v.Description = other.Description + } + + if (v.Id.IsUnknown() || v.Id.IsNull()) && !other.Id.IsUnknown() { + v.Id = other.Id + } + + if (v.ImageId.IsUnknown() || v.ImageId.IsNull()) && !other.ImageId.IsUnknown() { + v.ImageId = other.ImageId + } + + if (v.InstanceId.IsUnknown() || v.InstanceId.IsNull()) && !other.InstanceId.IsUnknown() { + v.InstanceId = other.InstanceId + } + + if (v.Name.IsUnknown() || v.Name.IsNull()) && !other.Name.IsUnknown() { + v.Name = other.Name + } + + if (v.Progress.IsUnknown() || v.Progress.IsNull()) && !other.Progress.IsUnknown() { + v.Progress = other.Progress + } + + if (v.RegionName.IsUnknown() || v.RegionName.IsNull()) && !other.RegionName.IsUnknown() { + v.RegionName = other.RegionName + } + + if (v.Regions.IsUnknown() || v.Regions.IsNull()) && !other.Regions.IsUnknown() { + v.Regions = other.Regions + } + + if (v.ResourceId.IsUnknown() || v.ResourceId.IsNull()) && !other.ResourceId.IsUnknown() { + v.ResourceId = other.ResourceId + } + + if (v.ServiceName.IsUnknown() || v.ServiceName.IsNull()) && !other.ServiceName.IsUnknown() { + v.ServiceName = other.ServiceName + } + + if (v.Size.IsUnknown() || v.Size.IsNull()) && !other.Size.IsUnknown() { + v.Size = other.Size + } + + if (v.SnapshotId.IsUnknown() || v.SnapshotId.IsNull()) && !other.SnapshotId.IsUnknown() { + v.SnapshotId = other.SnapshotId + } + + if (v.StartedAt.IsUnknown() || v.StartedAt.IsNull()) && !other.StartedAt.IsUnknown() { + v.StartedAt = other.StartedAt + } + + if (v.Status.IsUnknown() || v.Status.IsNull()) && !other.Status.IsUnknown() { + v.Status = other.Status + } + + if (v.SubOperations.IsUnknown() || v.SubOperations.IsNull()) && !other.SubOperations.IsUnknown() { + v.SubOperations = other.SubOperations + } + + if (v.Type.IsUnknown() || v.Type.IsNull()) && !other.Type.IsUnknown() { + v.Type = other.Type + } + + if (v.VolumeId.IsUnknown() || v.VolumeId.IsNull()) && !other.VolumeId.IsUnknown() { + v.VolumeId = other.Id + } + +} + +func (v CloudProjectVolumeModelOp) ToCreate() *CloudProjectVolumeModelOp { + res := &CloudProjectVolumeModelOp{} + + if !v.Description.IsUnknown() { + res.Description = v.Description + } + + if !v.ImageId.IsUnknown() { + res.ImageId = v.ImageId + } + + if !v.InstanceId.IsUnknown() { + res.InstanceId = v.InstanceId + } + + if !v.Name.IsUnknown() { + res.Name = v.Name + } + + if !v.Size.IsUnknown() { + res.Size = v.Size + } + + if !v.SnapshotId.IsUnknown() { + res.SnapshotId = v.SnapshotId + } + + if !v.Type.IsUnknown() { + res.Type = v.Type + } + + return res +} + +func (v CloudProjectVolumeModelOp) ToUpdate() *CloudProjectVolumeModelOp { + res := &CloudProjectVolumeModelOp{} + + if !v.Description.IsUnknown() { + res.Description = v.Description + } + + if !v.ImageId.IsUnknown() { + res.ImageId = v.ImageId + } + + if !v.InstanceId.IsUnknown() { + res.InstanceId = v.InstanceId + } + + if !v.Name.IsUnknown() { + res.Name = v.Name + } + + if !v.Size.IsUnknown() { + res.Size = v.Size + } + + if !v.SnapshotId.IsUnknown() { + res.SnapshotId = v.SnapshotId + } + + if !v.Type.IsUnknown() { + res.Type = v.Type + } + + return res +} + +func (v *CloudProjectVolumeModelOp) MarshalJSON() ([]byte, error) { + toMarshal := map[string]any{} + if !v.Description.IsNull() && !v.Description.IsUnknown() { + toMarshal["description"] = v.Description + } + if !v.ImageId.IsNull() && !v.ImageId.IsUnknown() { + toMarshal["imageId"] = v.ImageId + } + if !v.InstanceId.IsNull() && !v.InstanceId.IsUnknown() { + toMarshal["instanceId"] = v.InstanceId + } + if !v.Name.IsNull() && !v.Name.IsUnknown() { + toMarshal["name"] = v.Name + } + if !v.Size.IsNull() && !v.Size.IsUnknown() { + toMarshal["size"] = v.Size + } + if !v.SnapshotId.IsNull() && !v.SnapshotId.IsUnknown() { + toMarshal["snapshotId"] = v.SnapshotId + } + if !v.Type.IsNull() && !v.Type.IsUnknown() { + toMarshal["type"] = v.Type + } + + return json.Marshal(toMarshal) +} + +var _ basetypes.ObjectTypable = SubOperationsType{} + +type SubOperationsType struct { + basetypes.ObjectType +} + +func (t SubOperationsType) Equal(o attr.Type) bool { + other, ok := o.(SubOperationsType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t SubOperationsType) String() string { + return "SubOperationsType" +} + +func (t SubOperationsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + resourceIdAttribute, ok := attributes["resource_id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `resource_id is missing from object`) + + return nil, diags + } + + resourceIdVal, ok := resourceIdAttribute.(ovhtypes.TfStringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`resource_id expected to be ovhtypes.TfStringValue, was: %T`, resourceIdAttribute)) + } + + resourceTypeAttribute, ok := attributes["resource_type"] + + if !ok { + diags.AddError( + "Attribute Missing", + `resource_type is missing from object`) + + return nil, diags + } + + resourceTypeVal, ok := resourceTypeAttribute.(ovhtypes.TfStringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`resource_type expected to be ovhtypes.TfStringValue, was: %T`, resourceTypeAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return SubOperationsValue{ + ResourceId: resourceIdVal, + ResourceType: resourceTypeVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewSubOperationsValueNull() SubOperationsValue { + return SubOperationsValue{ + state: attr.ValueStateNull, + } +} + +func NewSubOperationsValueUnknown() SubOperationsValue { + return SubOperationsValue{ + state: attr.ValueStateUnknown, + } +} + +func NewSubOperationsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (SubOperationsValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing SubOperationsValue Attribute Value", + "While creating a SubOperationsValue value, a missing attribute value was detected. "+ + "A SubOperationsValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("SubOperationsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid SubOperationsValue Attribute Type", + "While creating a SubOperationsValue value, an invalid attribute value was detected. "+ + "A SubOperationsValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("SubOperationsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("SubOperationsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra SubOperationsValue Attribute Value", + "While creating a SubOperationsValue value, an extra attribute value was detected. "+ + "A SubOperationsValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra SubOperationsValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewSubOperationsValueUnknown(), diags + } + + resourceIdAttribute, ok := attributes["resource_id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `resource_id is missing from object`) + + return NewSubOperationsValueUnknown(), diags + } + + resourceIdVal, ok := resourceIdAttribute.(ovhtypes.TfStringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`resource_id expected to be ovhtypes.TfStringValue, was: %T`, resourceIdAttribute)) + } + + resourceTypeAttribute, ok := attributes["resource_type"] + + if !ok { + diags.AddError( + "Attribute Missing", + `resource_type is missing from object`) + + return NewSubOperationsValueUnknown(), diags + } + + resourceTypeVal, ok := resourceTypeAttribute.(ovhtypes.TfStringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`resource_type expected to be ovhtypes.TfStringValue, was: %T`, resourceTypeAttribute)) + } + + if diags.HasError() { + return NewSubOperationsValueUnknown(), diags + } + + return SubOperationsValue{ + ResourceId: resourceIdVal, + ResourceType: resourceTypeVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewSubOperationsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) SubOperationsValue { + object, diags := NewSubOperationsValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewSubOperationsValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t SubOperationsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewSubOperationsValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewSubOperationsValueUnknown(), nil + } + + if in.IsNull() { + return NewSubOperationsValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewSubOperationsValueMust(SubOperationsValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t SubOperationsType) ValueType(ctx context.Context) attr.Value { + return SubOperationsValue{} +} + +var _ basetypes.ObjectValuable = SubOperationsValue{} + +type SubOperationsValue struct { + ResourceId ovhtypes.TfStringValue `tfsdk:"resource_id" json:"resourceId"` + ResourceType ovhtypes.TfStringValue `tfsdk:"resource_type" json:"resourceType"` + state attr.ValueState +} + +func (v *SubOperationsValue) UnmarshalJSON(data []byte) error { + type JsonSubOperationsValue SubOperationsValue + + var tmp JsonSubOperationsValue + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + v.ResourceId = tmp.ResourceId + v.ResourceType = tmp.ResourceType + + v.state = attr.ValueStateKnown + + return nil +} + +func (v *SubOperationsValue) MergeWith(other *SubOperationsValue) { + + if (v.ResourceId.IsUnknown() || v.ResourceId.IsNull()) && !other.ResourceId.IsUnknown() { + v.ResourceId = other.ResourceId + } + + if (v.ResourceType.IsUnknown() || v.ResourceType.IsNull()) && !other.ResourceType.IsUnknown() { + v.ResourceType = other.ResourceType + } + + if (v.state == attr.ValueStateUnknown || v.state == attr.ValueStateNull) && other.state != attr.ValueStateUnknown { + v.state = other.state + } +} + +func (v SubOperationsValue) Attributes() map[string]attr.Value { + return map[string]attr.Value{ + "resourceId": v.ResourceId, + "resourceType": v.ResourceType, + } +} +func (v SubOperationsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 2) + + var val tftypes.Value + var err error + + attrTypes["resource_id"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["resource_type"] = basetypes.StringType{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 2) + + val, err = v.ResourceId.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["resource_id"] = val + + val, err = v.ResourceType.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["resource_type"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v SubOperationsValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v SubOperationsValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v SubOperationsValue) String() string { + return "SubOperationsValue" +} + +func (v SubOperationsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + objVal, diags := types.ObjectValue( + map[string]attr.Type{ + "resource_id": ovhtypes.TfStringType{}, + "resource_type": ovhtypes.TfStringType{}, + }, + map[string]attr.Value{ + "resource_id": v.ResourceId, + "resource_type": v.ResourceType, + }) + + return objVal, diags +} + +func (v SubOperationsValue) Equal(o attr.Value) bool { + other, ok := o.(SubOperationsValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.ResourceId.Equal(other.ResourceId) { + return false + } + + if !v.ResourceType.Equal(other.ResourceType) { + return false + } + + return true +} + +func (v SubOperationsValue) Type(ctx context.Context) attr.Type { + return SubOperationsType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v SubOperationsValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "resource_id": ovhtypes.TfStringType{}, + "resource_type": ovhtypes.TfStringType{}, + } +} diff --git a/ovh/resource_cloud_project_volume_test.go b/ovh/resource_cloud_project_volume_test.go new file mode 100644 index 00000000..2dce9ef1 --- /dev/null +++ b/ovh/resource_cloud_project_volume_test.go @@ -0,0 +1,44 @@ +package ovh + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccCloudProjectVolume_basic(t *testing.T) { + serviceName := os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST") + regionName := os.Getenv("OVH_CLOUD_PROJECT_REGION_TEST") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheckCloud(t); testAccCheckCloudProjectExists(t) }, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "ovh_cloud_project_volume" "volume" { + region_name = "%s" + service_name = "%s" + description = "test" + name = "test" + size = 15 + type = "classic" + } + `, + regionName, + serviceName, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("ovh_cloud_project_volume.volume", "region_name", os.Getenv("OVH_CLOUD_PROJECT_REGION_TEST")), + resource.TestCheckResourceAttr("ovh_cloud_project_volume.volume", "service_name", os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST")), + resource.TestCheckResourceAttrSet("ovh_cloud_project_volume.volume", "volume_id"), + resource.TestCheckResourceAttrSet("ovh_cloud_project_volume.volume", "type"), + resource.TestCheckResourceAttrSet("ovh_cloud_project_volume.volume", "description"), + resource.TestCheckResourceAttrSet("ovh_cloud_project_volume.volume", "name"), + ), + }, + }, + }) +} diff --git a/website/docs/r/cloud_project_region_volume.markdown b/website/docs/r/cloud_project_region_volume.markdown new file mode 100644 index 00000000..8cc1a542 --- /dev/null +++ b/website/docs/r/cloud_project_region_volume.markdown @@ -0,0 +1,44 @@ +--- +subcategory : "Managed update" +--- + +# ovh_cloud_project_volume + +Create volume in a public cloud project. + +## Example Usage + +Create a subscription + +```hcl +resource "ovh_cloud_project_volume" "vol" { + region_name = "xxx" + service_name = "yyyyy" + description = "Terraform volume" + name = "terrformName" + size = 15 + type = "classic" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_name` - The id of the public cloud project. If omitted, the `OVH_CLOUD_PROJECT_SERVICE` environment variable is used. **Changing this value recreates the resource.** +* `region_name` - A valid OVHcloud public cloud region name in which the volume will be available. Ex.: "GRA11". **Changing this value recreates the resource.** +* `description` - A description of the volume +* `name` - Name of the volume +* `size` - Size of the volume **Changing this value recreates the resource.** +* `type` - Type of the volume **Changing this value recreates the resource.** + +## Attributes Reference + +The following attributes are exported: + +* `service_name` - The id of the public cloud project. +* `region_name` - A valid OVHcloud public cloud region name in which the volume will be available. +* `description` - A description of the volume +* `name` - Name of the volume +* `size` - Size of the volume **Changing this value recreates the resource.** +* `id` - id of the volume **Changing this value recreates the resource.** \ No newline at end of file