From 9d9f25e060db8780f6002f7e376d7473b927b203 Mon Sep 17 00:00:00 2001 From: Antoine Beyet Date: Thu, 24 Aug 2023 21:59:45 +0200 Subject: [PATCH] fix(cloudProject): creation and import for cloudProject in the US --- ovh/order.go | 22 ++++++++ ovh/resource_cloud_project.go | 62 ++++++++++++++++++++-- ovh/types_me_order.go | 14 +++++ website/docs/r/cloud_project.html.markdown | 5 +- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/ovh/order.go b/ovh/order.go index d44665b19..d5f26b5ee 100644 --- a/ovh/order.go +++ b/ovh/order.go @@ -494,3 +494,25 @@ func waitForOrder(c *ovh.Client, id int64) resource.StateRefreshFunc { return r, r, nil } } + +func orderDetailOperations(c *ovh.Client, orderId int64, orderDetailId int64) ([]*MeOrderDetailOperation, error) { + log.Printf("[DEBUG] Will list order detail operations %d/%d", orderId, orderDetailId) + operationsIds := []int64{} + endpoint := fmt.Sprintf("/me/order/%d/details/%d/operations", orderId, orderDetailId) + if err := c.Get(endpoint, &operationsIds); err != nil { + return nil, fmt.Errorf("calling get %s:\n\t %q", endpoint, err) + } + + operations := make([]*MeOrderDetailOperation, len(operationsIds)) + for i, operationId := range operationsIds { + operation := &MeOrderDetailOperation{} + log.Printf("[DEBUG] Will read order detail operations %d/%d/%d", orderId, orderDetailId, operationId) + endpoint := fmt.Sprintf("/me/order/%d/details/%d/operations/%d", orderId, orderDetailId, operationId) + if err := c.Get(endpoint, operation); err != nil { + return nil, fmt.Errorf("calling get %s:\n\t %q", endpoint, err) + } + + operations[i] = operation + } + return operations, nil +} diff --git a/ovh/resource_cloud_project.go b/ovh/resource_cloud_project.go index b305e5eef..e5fc4d8c9 100644 --- a/ovh/resource_cloud_project.go +++ b/ovh/resource_cloud_project.go @@ -4,12 +4,17 @@ import ( "fmt" "log" "net/url" + "regexp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/ovh/go-ovh/ovh" "github.com/ovh/terraform-provider-ovh/ovh/helpers" ) +var ( + publicCloudProjectNameFormatRegex = regexp.MustCompile("^[0-9a-f]{12}4[0-9a-f]{19}$") +) + func resourceCloudProject() *schema.Resource { return &schema.Resource{ Create: resourceCloudProjectCreate, @@ -74,7 +79,7 @@ func resourceCloudProjectCreate(d *schema.ResourceData, meta interface{}) error } func resourceCloudProjectUpdate(d *schema.ResourceData, meta interface{}) error { - _, details, err := orderRead(d, meta) + order, details, err := orderRead(d, meta) if err != nil { return fmt.Errorf("Could not read cloud project order: %q", err) } @@ -82,6 +87,23 @@ func resourceCloudProjectUpdate(d *schema.ResourceData, meta interface{}) error config := meta.(*Config) serviceName := details[0].Domain + // in the US, for reasons too long to be detailled here, cloud project order domain is not the public cloud project id, but "*". + // There have been discussions to align US & EU, but they've failed. + // So we end up making a few extra queries to fetch the project id from operations details. + if !publicCloudProjectNameFormatRegex.MatchString(serviceName) { + orderDetailId := details[0].OrderDetailId + operations, err := orderDetailOperations(config.OVHClient, order.OrderId, orderDetailId) + if err != nil { + return fmt.Errorf("Could not read cloudProject order details operations: %q", err) + } + for _, operation := range operations { + if !publicCloudProjectNameFormatRegex.MatchString(operation.Resource.Name) { + continue + } + serviceName = operation.Resource.Name + } + } + log.Printf("[DEBUG] Will update cloudProject: %s", serviceName) opts := (&CloudProjectUpdateOpts{}).FromResource(d) endpoint := fmt.Sprintf("/cloud/project/%s", serviceName) @@ -93,7 +115,7 @@ func resourceCloudProjectUpdate(d *schema.ResourceData, meta interface{}) error } func resourceCloudProjectRead(d *schema.ResourceData, meta interface{}) error { - _, details, err := orderRead(d, meta) + order, details, err := orderRead(d, meta) if err != nil { return fmt.Errorf("Could not read cloudProject order: %q", err) } @@ -101,6 +123,23 @@ func resourceCloudProjectRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) serviceName := details[0].Domain + // in the US, for reasons too long to be detailled here, cloud project order domain is not the public cloud project id, but "*". + // There have been discussions to align US & EU, but they've failed. + // So we end up making a few extra queries to fetch the project id from operations details. + if !publicCloudProjectNameFormatRegex.MatchString(serviceName) { + orderDetailId := details[0].OrderDetailId + operations, err := orderDetailOperations(config.OVHClient, order.OrderId, orderDetailId) + if err != nil { + return fmt.Errorf("Could not read cloudProject order details operations: %q", err) + } + for _, operation := range operations { + if !publicCloudProjectNameFormatRegex.MatchString(operation.Resource.Name) { + continue + } + serviceName = operation.Resource.Name + } + } + log.Printf("[DEBUG] Will read cloudProject: %s", serviceName) r := &CloudProject{} endpoint := fmt.Sprintf("/cloud/project/%s", serviceName) @@ -119,7 +158,7 @@ func resourceCloudProjectRead(d *schema.ResourceData, meta interface{}) error { } func resourceCloudProjectDelete(d *schema.ResourceData, meta interface{}) error { - _, details, err := orderRead(d, meta) + order, details, err := orderRead(d, meta) if err != nil { return fmt.Errorf("Could not read cloudProject order: %q", err) } @@ -127,6 +166,23 @@ func resourceCloudProjectDelete(d *schema.ResourceData, meta interface{}) error config := meta.(*Config) serviceName := details[0].Domain + // in the US, for reasons too long to be detailled here, cloud project order domain is not the public cloud project id, but "*". + // There have been discussions to align US & EU, but they've failed. + // So we end up making a few extra queries to fetch the project id from operations details. + if !publicCloudProjectNameFormatRegex.MatchString(serviceName) { + orderDetailId := details[0].OrderDetailId + operations, err := orderDetailOperations(config.OVHClient, order.OrderId, orderDetailId) + if err != nil { + return fmt.Errorf("Could not read cloudProject order details operations: %q", err) + } + for _, operation := range operations { + if !publicCloudProjectNameFormatRegex.MatchString(operation.Resource.Name) { + continue + } + serviceName = operation.Resource.Name + } + } + id := d.Id() terminate := func() (string, error) { diff --git a/ovh/types_me_order.go b/ovh/types_me_order.go index 0523ed309..83e90d0f8 100644 --- a/ovh/types_me_order.go +++ b/ovh/types_me_order.go @@ -30,6 +30,20 @@ func (v MeOrderDetail) ToMap() map[string]interface{} { return obj } +type MeOrderDetailOperation struct { + Status string `json:"status"` + ID int `json:"id"` + Type string `json:"type"` + Resource MeOrderDetailOperationResource `json:"resource"` + Quantity int `json:"quantity"` +} + +type MeOrderDetailOperationResource struct { + Name string `json:"name"` + State string `json:"state"` + DisplayName string `json:"displayName"` +} + type MeOrderPaymentOpts struct { PaymentMean string `json:"paymentMean"` PaymentMeanId *int64 `json:"paymentMeanId,omitEmpty"` diff --git a/website/docs/r/cloud_project.html.markdown b/website/docs/r/cloud_project.html.markdown index 4320df59d..e06f9ec34 100644 --- a/website/docs/r/cloud_project.html.markdown +++ b/website/docs/r/cloud_project.html.markdown @@ -23,6 +23,7 @@ data "ovh_order_cart_product_plan" "cloud" { price_capacity = "renew" product = "cloud" plan_code = "project.2018" + # plan_code = "project" # when running in the US } resource "ovh_cloud_project" "my_cloud_project" { @@ -37,6 +38,8 @@ resource "ovh_cloud_project" "my_cloud_project" { } ``` +-> __WARNING__ Currently, the OVHcloud Terraform provider does not support deletion of a public cloud project in the US. Removal is possible by manually deleting the project and then manually removing the public cloud project from terraform state. + ## Argument Reference The following arguments are supported: @@ -46,7 +49,7 @@ The following arguments are supported: * `ovh_subsidiary` - (Required) OVHcloud Subsidiary * `plan` - (Required) Product Plan to order * `duration` - (Required) duration - * `plan_code` - (Required) Plan code + * `plan_code` - (Required) Plan code. This value must be adapted depending on your `OVH_ENDPOINT` value. It's `project.2018` for `ovh-{eu,ca}` and `project` when using `ovh-us`. * `pricing_mode` - (Required) Pricing model identifier * `catalog_name` - Catalog name * `configuration` - (Optional) Representation of a configuration item for personalizing product