From b2a3bded9998e85137014c44e30305b4814d76f6 Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Sat, 28 Nov 2020 10:50:27 +0800 Subject: [PATCH] Add images_image rsources support (#706) --- docs/resources/images_image.md | 136 +++--- docs/resources/images_image_v2.md | 100 +++++ .../data_source_huaweicloud_cce_cluster_v3.go | 2 +- huaweicloud/provider.go | 2 +- .../resource_huaweicloud_cce_cluster_v3.go | 2 +- .../resource_huaweicloud_dns_recordset_v2.go | 4 +- .../resource_huaweicloud_images_image.go | 391 ++++++++++++++++++ .../resource_huaweicloud_images_image_test.go | 201 +++++++++ .../openstack/ims/v2/cloudimages/requests.go | 276 +++++++++++++ .../openstack/ims/v2/cloudimages/results.go | 157 +++++++ .../ims/v2/cloudimages/results_job.go | 92 +++++ .../openstack/ims/v2/cloudimages/urls.go | 46 +++ .../openstack/ims/v2/tags/requests.go | 61 +++ .../openstack/ims/v2/tags/results.go | 28 ++ .../golangsdk/openstack/ims/v2/tags/urls.go | 17 + vendor/modules.txt | 2 + 16 files changed, 1454 insertions(+), 63 deletions(-) create mode 100644 docs/resources/images_image_v2.md create mode 100644 huaweicloud/resource_huaweicloud_images_image.go create mode 100644 huaweicloud/resource_huaweicloud_images_image_test.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/requests.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results_job.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/urls.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/requests.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/results.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/urls.go diff --git a/docs/resources/images_image.md b/docs/resources/images_image.md index 6b36f9bc66..56df2f4afa 100644 --- a/docs/resources/images_image.md +++ b/docs/resources/images_image.md @@ -4,18 +4,55 @@ subcategory: "Image Management Service (IMS)" # huaweicloud\_images\_image -Manages a Image resource within HuaweiCloud IMS. -This is an alternative to `huaweicloud_images_image_v2` +Manages an Image resource within HuaweiCloud IMS. ## Example Usage +### Creating an image from ECS + +```hcl +data "huaweicloud_availability_zones" "test" {} + +data "huaweicloud_vpc_subnet" "test" { + name = "subnet-default" +} + +resource "huaweicloud_compute_instance" "test" { + name = "%s" + image_name = "Ubuntu 18.04 server 64bit" + security_groups = ["default"] + availability_zone = data.huaweicloud_availability_zones.test.names[0] + + network { + uuid = data.huaweicloud_vpc_subnet.test.id + } +} + +resource "huaweicloud_images_image" "test" { + name = "%s" + instance_id = huaweicloud_compute_instance.test.id + description = "created by Terraform" + + tags = { + foo = "bar" + key = "value" + } +} +``` + +### Creating an image from OBS bucket + ```hcl -resource "huaweicloud_images_image" "rancheros" { - name = "RancherOS" - image_source_url = "https://releases.rancher.com/os/latest/rancheros-openstack.img" - container_format = "bare" - disk_format = "qcow2" - tags = ["foo.bar", "tag.value"] +resource "huaweicloud_images_image" "ims_test_file" { + name = "ims_test_file" + image_url = "ims-image:centos70.qcow2" + min_disk = 40 + description = "Create an image from the OBS bucket." + + tags = { + foo = "bar1" + key = "value" + } } ``` @@ -23,79 +60,62 @@ resource "huaweicloud_images_image" "rancheros" { The following arguments are supported: -* `container_format` - (Required) The container format. Must be "bare". - -* `disk_format` - (Required) The disk format. Must be one of "qcow2", "vhd". +* `name` - (Required, String) The name of the image. -* `local_file_path` - (Optional) This is the filepath of the raw image file - that will be uploaded to Glance. Conflicts with `image_source_url`. +* `description` - (Optional, String, ForceNew) A description of the image. -* `image_cache_path` - (Optional) This is the directory where the images will - be downloaded. Images will be stored with a filename corresponding to - the url's md5 hash. Defaults to "$HOME/.terraform/image_cache" +* `min_ram` - (Optional, Int, ForceNew) The minimum memory of the image in the unit of MB. + The default value is 0, indicating that the memory is not restricted. -* `image_source_url` - (Optional) This is the url of the raw image that will - be downloaded in the `image_cache_path` before being uploaded to Glance. - Glance is able to download image from internet but the `golangsdk` library - does not yet provide a way to do so. - Conflicts with `local_file_path`. +* `max_ram` - (Optional, Int, ForceNew) The maximum memory of the image in the unit of MB. -* `min_disk_gb` - (Optional) Amount of disk space (in GB) required to boot image. - Defaults to 0. +* `tags` - (Optional, Map) The tags of the image. -* `min_ram_mb` - (Optional) Amount of ram (in MB) required to boot image. - Defauts to 0. +* `instance_id` - (Optional, String, ForceNew) The ID of the ECS that needs to be converted into an image. + This parameter is mandatory when you create a privete image from an ECS. -* `name` - (Required) The name of the image. +* `image_url` - (Optional, String, ForceNew) The URL of the external image file in the OBS bucket. + This parameter is mandatory when you create a private image from an external file + uploaded to an OBS bucket. The format is *OBS bucket name:Image file name*. -* `protected` - (Optional) If true, image will not be deletable. - Defaults to false. +* `min_disk` - (Optional, Int, ForceNew) The minimum size of the system disk in the unit of GB. + This parameter is mandatory when you create a private image from an external file + uploaded to an OBS bucket. The value ranges from 1 GB to 1024 GB. -* `region` - (Optional) The region in which to create the V2 Glance client. - A Glance client is needed to create an Image that can be used with - a compute instance. If omitted, the `region` argument of the provider - is used. Changing this creates a new Image. +* `os_version` - (Optional, String, ForceNew) The OS version. + This parameter is valid when you create a private image from an external file + uploaded to an OBS bucket. -* `tags` - (Optional) The tags of the image. It must be a list of strings. - At this time, it is not possible to delete all tags of an image. +* `is_config` - (Optional, Bool, ForceNew) If automatic configuration is required, set the value to true. + Otherwise, set the value to false. -* `visibility` - (Optional) The visibility of the image. Must be "private". - The ability to set the visibility depends upon the configuration of - the HuaweiCloud cloud. +* `cmk_id` - (Optional, String, ForceNew) The master key used for encrypting an image. -Note: The `properties` attribute handling in the golangsdk library is currently buggy -and needs to be fixed before being implemented in this resource. +* `type` - (Optional, String, ForceNew) The image type. Must be one of `ECS`, `FusionCompute`, `BMS`, or `Ironic`. ## Attributes Reference The following attributes are exported: -* `checksum` - The checksum of the data associated with the image. -* `created_at` - The date the image was created. -* `file` - the trailing path after the glance - endpoint that represent the location of the image - or the path to retrieve it. -* `id` - A unique ID assigned by Glance. -* `metadata` - The metadata associated with the image. - Image metadata allow for meaningfully define the image properties - and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html. -* `owner` - The id of the huaweicloud user who owns the image. -* `schema` - The path to the JSON-schema that represent - the image or image -* `size_bytes` - The size in bytes of the data associated with the image. -* `status` - The status of the image. It can be "queued", "active" - or "saving". -* `update_at` - The date the image was last updated. +* `id` - A unique ID assigned by IMS. +* `visibility` - Whether the image is visible to other tenants. + +* `data_origin` - The image resource. The pattern can be 'instance,*instance_id*' or 'file,*image_url*'. + +* `disk_format` - The image file format. The value can be `vhd`, `zvhd`, `raw`, `zvhd2`, or `qcow2`. + +* `image_size` - The size(bytes) of the image file format. ## Timeouts This resource provides the following timeouts configuration options: -- `create` - Default is 30 minute. +- `create` - Default is 10 minute. +- `delete` - Default is 3 minute. ## Import Images can be imported using the `id`, e.g. -``` -$ terraform import huaweicloud_images_image.rancheros 89c60255-9bd6-460c-822a-e2b959ede9d2 +```sh +terraform import huaweicloud_images_image.my_image 7886e623-f1b3-473e-b882-67ba1c35887f ``` diff --git a/docs/resources/images_image_v2.md b/docs/resources/images_image_v2.md new file mode 100644 index 0000000000..9161acf3ed --- /dev/null +++ b/docs/resources/images_image_v2.md @@ -0,0 +1,100 @@ +--- +subcategory: "Deprecated" +--- + +# huaweicloud\_images\_image\_v2 + +Manages a Image resource within HuaweiCloud IMS. + +## Example Usage + +```hcl +resource "huaweicloud_images_image_v2" "rancheros" { + name = "RancherOS" + image_source_url = "https://releases.rancher.com/os/latest/rancheros-openstack.img" + container_format = "bare" + disk_format = "qcow2" + tags = ["foo.bar", "tag.value"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `container_format` - (Required) The container format. Must be "bare". + +* `disk_format` - (Required) The disk format. Must be one of "qcow2", "vhd". + +* `local_file_path` - (Optional) This is the filepath of the raw image file + that will be uploaded to Glance. Conflicts with `image_source_url`. + +* `image_cache_path` - (Optional) This is the directory where the images will + be downloaded. Images will be stored with a filename corresponding to + the url's md5 hash. Defaults to "$HOME/.terraform/image_cache" + +* `image_source_url` - (Optional) This is the url of the raw image that will + be downloaded in the `image_cache_path` before being uploaded to Glance. + Glance is able to download image from internet but the `golangsdk` library + does not yet provide a way to do so. + Conflicts with `local_file_path`. + +* `min_disk_gb` - (Optional) Amount of disk space (in GB) required to boot image. + Defaults to 0. + +* `min_ram_mb` - (Optional) Amount of ram (in MB) required to boot image. + Defauts to 0. + +* `name` - (Required) The name of the image. + +* `protected` - (Optional) If true, image will not be deletable. + Defaults to false. + +* `region` - (Optional) The region in which to create the V2 Glance client. + A Glance client is needed to create an Image that can be used with + a compute instance. If omitted, the `region` argument of the provider + is used. Changing this creates a new Image. + +* `tags` - (Optional) The tags of the image. It must be a list of strings. + At this time, it is not possible to delete all tags of an image. + +* `visibility` - (Optional) The visibility of the image. Must be "private". + The ability to set the visibility depends upon the configuration of + the HuaweiCloud cloud. + +Note: The `properties` attribute handling in the golangsdk library is currently buggy +and needs to be fixed before being implemented in this resource. + +## Attributes Reference + +The following attributes are exported: + +* `checksum` - The checksum of the data associated with the image. +* `created_at` - The date the image was created. +* `file` - the trailing path after the glance + endpoint that represent the location of the image + or the path to retrieve it. +* `id` - A unique ID assigned by Glance. +* `metadata` - The metadata associated with the image. + Image metadata allow for meaningfully define the image properties + and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html. +* `owner` - The id of the huaweicloud user who owns the image. +* `schema` - The path to the JSON-schema that represent + the image or image +* `size_bytes` - The size in bytes of the data associated with the image. +* `status` - The status of the image. It can be "queued", "active" + or "saving". +* `update_at` - The date the image was last updated. + + +## Timeouts +This resource provides the following timeouts configuration options: +- `create` - Default is 30 minute. + +## Import + +Images can be imported using the `id`, e.g. + +``` +$ terraform import huaweicloud_images_image_v2.rancheros 89c60255-9bd6-460c-822a-e2b959ede9d2 +``` diff --git a/huaweicloud/data_source_huaweicloud_cce_cluster_v3.go b/huaweicloud/data_source_huaweicloud_cce_cluster_v3.go index d5f7d6daab..61f3432e20 100644 --- a/huaweicloud/data_source_huaweicloud_cce_cluster_v3.go +++ b/huaweicloud/data_source_huaweicloud_cce_cluster_v3.go @@ -210,7 +210,7 @@ func dataSourceCCEClusterV3Read(d *schema.ResourceData, meta interface{}) error cert, err := r.Extract() if err != nil { - log.Printf("Error retrieving opentelekomcloud CCE cluster cert: %s", err) + log.Printf("Error retrieving HuaweiCloud CCE cluster cert: %s", err) } //Set Certificate Clusters diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index fc09c05f73..2ea3a36f19 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -377,7 +377,7 @@ func Provider() terraform.ResourceProvider { "huaweicloud_identity_project": ResourceIdentityProjectV3(), "huaweicloud_identity_role_assignment": ResourceIdentityRoleAssignmentV3(), "huaweicloud_identity_user": ResourceIdentityUserV3(), - "huaweicloud_images_image": resourceImagesImageV2(), + "huaweicloud_images_image": resourceImsImage(), "huaweicloud_kms_key": resourceKmsKeyV1(), "huaweicloud_lb_certificate": resourceCertificateV2(), "huaweicloud_lb_l7policy": resourceL7PolicyV2(), diff --git a/huaweicloud/resource_huaweicloud_cce_cluster_v3.go b/huaweicloud/resource_huaweicloud_cce_cluster_v3.go index e9627c8f7b..a68e7aef6a 100644 --- a/huaweicloud/resource_huaweicloud_cce_cluster_v3.go +++ b/huaweicloud/resource_huaweicloud_cce_cluster_v3.go @@ -353,7 +353,7 @@ func resourceCCEClusterV3Read(d *schema.ResourceData, meta interface{}) error { cert, err := r.Extract() if err != nil { - log.Printf("Error retrieving opentelekomcloud CCE cluster cert: %s", err) + log.Printf("Error retrieving HuaweiCloud CCE cluster cert: %s", err) } //Set Certificate Clusters diff --git a/huaweicloud/resource_huaweicloud_dns_recordset_v2.go b/huaweicloud/resource_huaweicloud_dns_recordset_v2.go index 8cef9a25df..aa68dc9f6c 100644 --- a/huaweicloud/resource_huaweicloud_dns_recordset_v2.go +++ b/huaweicloud/resource_huaweicloud_dns_recordset_v2.go @@ -192,12 +192,12 @@ func resourceDNSRecordSetV2Read(d *schema.ResourceData, meta interface{}) error } resourceTags, err := tags.Get(dnsClient, resourceType, recordsetID).Extract() if err != nil { - return fmt.Errorf("Error fetching OpenTelekomCloud DNS record set tags: %s", err) + return fmt.Errorf("Error fetching HuaweiCloud DNS record set tags: %s", err) } tagmap := tagsToMap(resourceTags.Tags) if err := d.Set("tags", tagmap); err != nil { - return fmt.Errorf("Error saving tags for OpenTelekomCloud DNS record set %s: %s", recordsetID, err) + return fmt.Errorf("Error saving tags for HuaweiCloud DNS record set %s: %s", recordsetID, err) } return nil diff --git a/huaweicloud/resource_huaweicloud_images_image.go b/huaweicloud/resource_huaweicloud_images_image.go new file mode 100644 index 0000000000..3c27e0cfdf --- /dev/null +++ b/huaweicloud/resource_huaweicloud_images_image.go @@ -0,0 +1,391 @@ +package huaweicloud + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/huaweicloud/golangsdk" + + imageservice_v2 "github.com/huaweicloud/golangsdk/openstack/imageservice/v2/images" + "github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages" + "github.com/huaweicloud/golangsdk/openstack/ims/v2/tags" +) + +func resourceImsImage() *schema.Resource { + return &schema.Resource{ + Create: resourceImsImageCreate, + Read: resourceImsImageRead, + Update: resourceImsImageUpdate, + Delete: resourceImsImageDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(3 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "tags": { + Type: schema.TypeMap, + Optional: true, + }, + "max_ram": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "min_ram": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + // instance_id is required for creating an image from an ECS + "instance_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"image_url"}, + }, + // image_url and min_disk are required for creating an image from an OBS + "image_url": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"instance_id"}, + }, + "min_disk": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"instance_id"}, + }, + // following are valid for creating an image from an OBS + "os_version": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "is_config": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + "cmk_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "ECS", "FusionCompute", "BMS", "Ironic", + }, true), + }, + // following are additional attributus + "visibility": { + Type: schema.TypeString, + Computed: true, + }, + "data_origin": { + Type: schema.TypeString, + Computed: true, + }, + "disk_format": { + Type: schema.TypeString, + Computed: true, + }, + "image_size": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceContainerImageTags(d *schema.ResourceData) []cloudimages.ImageTag { + var tags []cloudimages.ImageTag + + image_tags := d.Get("tags").(map[string]interface{}) + for key, val := range image_tags { + tagRequest := cloudimages.ImageTag{ + Key: key, + Value: val.(string), + } + tags = append(tags, tagRequest) + } + return tags +} + +func resourceImsImageCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + ims_Client, err := config.imageV2Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud image client: %s", err) + } + + if !hasFilledOpt(d, "instance_id") && !hasFilledOpt(d, "image_url") { + return fmt.Errorf("Error creating HuaweiCloud IMS: " + + "Either 'instance_id' or 'image_url' must be specified") + } + + v := new(cloudimages.JobResponse) + image_tags := resourceContainerImageTags(d) + if hasFilledOpt(d, "instance_id") { + createOpts := &cloudimages.CreateByServerOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + InstanceId: d.Get("instance_id").(string), + MaxRam: d.Get("max_ram").(int), + MinRam: d.Get("min_ram").(int), + ImageTags: image_tags, + } + log.Printf("[DEBUG] Create Options: %#v", createOpts) + v, err = cloudimages.CreateImageByServer(ims_Client, createOpts).ExtractJobResponse() + } else { + if !hasFilledOpt(d, "min_disk") { + return fmt.Errorf("Error creating HuaweiCloud IMS: 'min_disk' must be specified") + } + + createOpts := &cloudimages.CreateByOBSOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + ImageUrl: d.Get("image_url").(string), + MinDisk: d.Get("min_disk").(int), + MaxRam: d.Get("max_ram").(int), + MinRam: d.Get("min_ram").(int), + OsVersion: d.Get("os_version").(string), + IsConfig: d.Get("is_config").(bool), + CmkId: d.Get("cmk_id").(string), + Type: d.Get("type").(string), + ImageTags: image_tags, + } + log.Printf("[DEBUG] Create Options: %#v", createOpts) + v, err = cloudimages.CreateImageByOBS(ims_Client, createOpts).ExtractJobResponse() + } + + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud IMS: %s", err) + } + log.Printf("[INFO] IMS Job ID: %s", v.JobID) + + // Wait for the ims to become available. + log.Printf("[DEBUG] Waiting for IMS to become available") + err = cloudimages.WaitForJobSuccess(ims_Client, int(d.Timeout(schema.TimeoutCreate)/time.Second), v.JobID) + if err != nil { + return err + } + + entity, err := cloudimages.GetJobEntity(ims_Client, v.JobID, "image_id") + if err != nil { + return err + } + + if id, ok := entity.(string); ok { + log.Printf("[INFO] IMS ID: %s", id) + // Store the ID now + d.SetId(id) + return resourceImsImageRead(d, meta) + } + return fmt.Errorf("Unexpected conversion error in resourceImsImageCreate.") +} + +func getCloudimage(client *golangsdk.ServiceClient, id string) (*cloudimages.Image, error) { + listOpts := &cloudimages.ListOpts{ + ID: id, + Limit: 1, + } + allPages, err := cloudimages.List(client, listOpts).AllPages() + if err != nil { + return nil, fmt.Errorf("Unable to query images: %s", err) + } + + allImages, err := cloudimages.ExtractImages(allPages) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve images: %s", err) + } + + if len(allImages) < 1 { + return nil, fmt.Errorf("Unable to find images %s: Maybe not existed", id) + } + + img := allImages[0] + if img.ID != id { + return nil, fmt.Errorf("Unexpected images ID") + } + log.Printf("[DEBUG] Retrieved Image %s: %#v", id, img) + return &img, nil +} + +func resourceImsImageRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + ims_Client, err := config.imageV2Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud image client: %s", err) + } + + img, err := getCloudimage(ims_Client, d.Id()) + if err != nil { + return fmt.Errorf("Image %s not found: %s", d.Id(), err) + } + log.Printf("[DEBUG] Retrieved Image %s: %#v", d.Id(), img) + + d.Set("name", img.Name) + d.Set("visibility", img.Visibility) + d.Set("file", img.File) + d.Set("schema", img.Schema) + d.Set("data_origin", img.DataOrigin) + d.Set("disk_format", img.DiskFormat) + d.Set("image_size", img.ImageSize) + + // Set image tags + Taglist, err := tags.Get(ims_Client, d.Id()).Extract() + if err != nil { + return fmt.Errorf("Error fetching HuaweiCloud image tags: %s", err) + } + + tagmap := make(map[string]string) + for _, val := range Taglist.Tags { + tagmap[val.Key] = val.Value + } + if err := d.Set("tags", tagmap); err != nil { + return fmt.Errorf("[DEBUG] Error saving tags for HuaweiCloud image (%s): %s", d.Id(), err) + } + return nil +} + +func setTagForImage(d *schema.ResourceData, meta interface{}, imageID string, tagmap map[string]interface{}) error { + config := meta.(*Config) + client, err := config.imageV2Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud image client: %s", err) + } + + rId := imageID + taglist := []tags.Tag{} + for k, v := range tagmap { + tag := tags.Tag{ + Key: k, + Value: v.(string), + } + taglist = append(taglist, tag) + } + + createOpts := tags.BatchOpts{Action: tags.ActionCreate, Tags: taglist} + createTags := tags.BatchAction(client, rId, createOpts) + if createTags.Err != nil { + return fmt.Errorf("Error creating HuaweiCloud image tags: %s", createTags.Err) + } + + return nil +} + +func resourceImsImageUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + ims_Client, err := config.imageV2Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud image client: %s", err) + } + + if d.HasChange("name") { + updateOpts := make(imageservice_v2.UpdateOpts, 0) + v := imageservice_v2.ReplaceImageName{NewName: d.Get("name").(string)} + updateOpts = append(updateOpts, v) + + log.Printf("[DEBUG] Update Options: %#v", updateOpts) + _, err = imageservice_v2.Update(ims_Client, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating image: %s", err) + } + } + + if d.HasChange("tags") { + oldTags, err := tags.Get(ims_Client, d.Id()).Extract() + if err != nil { + return fmt.Errorf("Error fetching HuaweiCloud image tags: %s", err) + } + if len(oldTags.Tags) > 0 { + deleteopts := tags.BatchOpts{Action: tags.ActionDelete, Tags: oldTags.Tags} + deleteTags := tags.BatchAction(ims_Client, d.Id(), deleteopts) + if deleteTags.Err != nil { + return fmt.Errorf("Error deleting HuaweiCloud image tags: %s", deleteTags.Err) + } + } + + if hasFilledOpt(d, "tags") { + tagmap := d.Get("tags").(map[string]interface{}) + if len(tagmap) > 0 { + log.Printf("[DEBUG] Setting tags: %v", tagmap) + err = setTagForImage(d, meta, d.Id(), tagmap) + if err != nil { + return fmt.Errorf("Error updating HuaweiCloud tags of image:%s", err) + } + } + } + } + + return resourceImsImageRead(d, meta) +} + +func resourceImsImageDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + imageClient, err := config.imageV2Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud image client: %s", err) + } + + log.Printf("[DEBUG] Deleting Image %s", d.Id()) + if err := imageservice_v2.Delete(imageClient, d.Id()).Err; err != nil { + return fmt.Errorf("Error deleting Image: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"ACTIVE"}, + Target: []string{"DELETED"}, + Refresh: waitForImageDelete(imageClient, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error deleting Huaweicloud Image: %s", err) + } + + d.SetId("") + return nil +} + +func waitForImageDelete(imageClient *golangsdk.ServiceClient, imageId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + r, err := imageservice_v2.Get(imageClient, imageId).Extract() + if err != nil { + if _, ok := err.(golangsdk.ErrDefault404); ok { + log.Printf("[INFO] Successfully deleted Huaweicloud image %s", imageId) + return r, "DELETED", nil + } + return r, "ACTIVE", err + } + + return r, "ACTIVE", nil + } +} diff --git a/huaweicloud/resource_huaweicloud_images_image_test.go b/huaweicloud/resource_huaweicloud_images_image_test.go new file mode 100644 index 0000000000..d6e4a2503b --- /dev/null +++ b/huaweicloud/resource_huaweicloud_images_image_test.go @@ -0,0 +1,201 @@ +package huaweicloud + +import ( + "fmt" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages" + "github.com/huaweicloud/golangsdk/openstack/ims/v2/tags" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccImsImage_basic(t *testing.T) { + var image cloudimages.Image + + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(5)) + rNameUpdate := rName + "-update" + resourceName := "huaweicloud_images_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckImsImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccImsImage_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckImsImageExists(resourceName, &image), + testAccCheckImsImageTags(resourceName, "foo", "bar"), + testAccCheckImsImageTags(resourceName, "key", "value"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + ), + }, + { + Config: testAccImsImage_update(rNameUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckImsImageExists(resourceName, &image), + testAccCheckImsImageTags(resourceName, "foo", "bar"), + testAccCheckImsImageTags(resourceName, "key", "value1"), + testAccCheckImsImageTags(resourceName, "key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "name", rNameUpdate), + ), + }, + }, + }) +} + +func testAccCheckImsImageDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + imageClient, err := config.imageV2Client(HW_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud Image: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "huaweicloud_images_image" { + continue + } + + _, err := getCloudimage(imageClient, rs.Primary.ID) + if err == nil { + return fmt.Errorf("Image still exists") + } + } + + return nil +} + +func testAccCheckImsImageExists(n string, image *cloudimages.Image) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("IMS Resource not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + imageClient, err := config.imageV2Client(HW_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud Image: %s", err) + } + + found, err := getCloudimage(imageClient, rs.Primary.ID) + if err != nil { + return err + } + + *image = *found + return nil + } +} + +func testAccCheckImsImageTags(n string, k string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("IMS Resource not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + imageClient, err := config.imageV2Client(HW_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud image client: %s", err) + } + + found, err := tags.Get(imageClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.Tags == nil { + return fmt.Errorf("IMS Tags not found") + } + + for _, tag := range found.Tags { + if k != tag.Key { + continue + } + + if v == tag.Value { + return nil + } + return fmt.Errorf("Bad value for %s: %s", k, tag.Value) + } + return fmt.Errorf("Tag not found: %s", k) + } +} + +func testAccImsImage_basic(rName string) string { + return fmt.Sprintf(` +data "huaweicloud_availability_zones" "test" {} + +data "huaweicloud_vpc_subnet" "test" { + name = "subnet-default" +} + +resource "huaweicloud_compute_instance" "test" { + name = "%s" + image_name = "Ubuntu 18.04 server 64bit" + security_groups = ["default"] + availability_zone = data.huaweicloud_availability_zones.test.names[0] + + network { + uuid = data.huaweicloud_vpc_subnet.test.id + } +} + +resource "huaweicloud_images_image" "test" { + name = "%s" + instance_id = huaweicloud_compute_instance.test.id + description = "created by TerraformAccTest" + + tags = { + foo = "bar" + key = "value" + } +} +`, rName, rName) +} + +func testAccImsImage_update(rName string) string { + return fmt.Sprintf(` +data "huaweicloud_availability_zones" "test" {} + +data "huaweicloud_vpc_subnet" "test" { + name = "subnet-default" +} + +resource "huaweicloud_compute_instance" "test" { + name = "%s" + image_name = "Ubuntu 18.04 server 64bit" + security_groups = ["default"] + availability_zone = data.huaweicloud_availability_zones.test.names[0] + + network { + uuid = data.huaweicloud_vpc_subnet.test.id + } +} + +resource "huaweicloud_images_image" "test" { + name = "%s" + instance_id = huaweicloud_compute_instance.test.id + description = "created by TerraformAccTest" + + tags = { + foo = "bar" + key = "value1" + key2 = "value2" + } +} +`, rName, rName) +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/requests.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/requests.go new file mode 100644 index 0000000000..725e870bc2 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/requests.go @@ -0,0 +1,276 @@ +package cloudimages + +import ( + "fmt" + "net/url" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +type ListOpts struct { + Isregistered string `q:"__isregistered"` + Imagetype string `q:"__imagetype"` + Protected bool `q:"protected"` + Visibility string `q:"visibility"` + Owner string `q:"owner"` + ID string `q:"id"` + Status string `q:"status"` + Name string `q:"name"` + ContainerFormat string `q:"container_format"` + DiskFormat string `q:"disk_format"` + MinRam int `q:"min_ram"` + MinDisk int `q:"min_disk"` + OsBit string `q:"__os_bit"` + Platform string `q:"__platform"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + OsType string `q:"__os_type"` + Tag string `q:"tag"` + MemberStatus string `q:"member_status"` + SupportKvm string `q:"__support_kvm"` + SupportXen string `q:"__support_xen"` + SupportLargeMemory string `q:"__support_largememory"` + SupportDiskintensive string `q:"__support_diskintensive"` + SupportHighperformance string `q:"__support_highperformance"` + SupportXenGpuType string `q:"__support_xen_gpu_type"` + SupportKvmGpuType string `q:"__support_kvm_gpu_type"` + SupportXenHana string `q:"__support_xen_hana"` + SupportKvmInfiniband string `q:"__support_kvm_infiniband"` + VirtualEnvType string `q:"virtual_env_type"` + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery +} + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List implements images list request +func List(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + imagePage := ImagePage{ + serviceURL: client.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create an image. +type CreateByServerOpts struct { + // the name of the system disk image + Name string `json:"name" required:"true"` + // Description of the image + Description string `json:"description,omitempty"` + // server id to be converted + InstanceId string `json:"instance_id" required:"true"` + // the data disks to be converted + DataImages []DataImage `json:"data_images,omitempty"` + // image label "key.value" + Tags []string `json:"tags,omitempty"` + // One or more tag key and value pairs to associate with the image + ImageTags []ImageTag `json:"image_tags,omitempty"` + // the maximum memory of the image in the unit of MB + MaxRam int `json:"max_ram,omitempty"` + // the minimum memory of the image in the unit of MB + MinRam int `json:"min_ram,omitempty"` +} + +// CreateOpts represents options used to create an image. +type CreateByOBSOpts struct { + // the name of the system disk image + Name string `json:"name" required:"true"` + // Description of image + Description string `json:"description,omitempty"` + // the OS version + OsVersion string `json:"os_version,omitempty"` + // the URL of the external image file in the OBS bucket + ImageUrl string `json:"image_url" required:"true"` + // the minimum size of the system disk in the unit of GB + MinDisk int `json:"min_disk" required:"true"` + //whether automatic configuration is enabled,the value can be true or false + IsConfig bool `json:"is_config,omitempty"` + // the master key used for encrypting an image + CmkId string `json:"cmk_id,omitempty"` + // image label "key.value" + Tags []string `json:"tags,omitempty"` + // One or more tag key and value pairs to associate with the image + ImageTags []ImageTag `json:"image_tags,omitempty"` + // the image type, the value can be ECS,BMS,FusionCompute, or Ironic + Type string `json:"type,omitempty"` + // the maximum memory of the image in the unit of MB + MaxRam int `json:"max_ram,omitempty"` + // the minimum memory of the image in the unit of MB + MinRam int `json:"min_ram,omitempty"` +} + +// CreateOpts represents options used to create an image. +type CreateDataImageByServerOpts struct { + // the data disks to be converted + DataImages []DataImage `json:"data_images" required:"true"` +} + +// CreateOpts represents options used to create an image. +type CreateDataImageByOBSOpts struct { + // the name of the data disk image + Name string `json:"name" required:"true"` + // Description of image + Description string `json:"description,omitempty"` + // the OS type + OsType string `json:"os_type" required:"true"` + // the URL of the external image file in the OBS bucket + ImageUrl string `json:"image_url" required:"true"` + // the minimum size of the system disk in the unit of GB + MinDisk int `json:"min_disk" required:"true"` + // the master key used for encrypting an image + CmkId string `json:"cmk_id,omitempty"` +} + +type DataImage struct { + // the data disk image name + Name string `json:"name" required:"true"` + // the data disk ID + VolumeId string `json:"volume_id" required:"true"` + // information about the data disk + Description string `json:"description,omitempty"` + // the data disk image tags + Tags []string `json:"tags,omitempty"` +} + +type ImageTag struct { + Key string `json:"key" required:"true"` + Value string `json:"value,omitempty"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateByServerOpts. +func (opts CreateByServerOpts) ToImageCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +func (opts CreateByOBSOpts) ToImageCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +func (opts CreateDataImageByServerOpts) ToImageCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +func (opts CreateDataImageByOBSOpts) ToImageCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Create implements create image request. +func CreateImageByServer(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r JobResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(createURL(client), b, &r.Body, &golangsdk.RequestOpts{OkCodes: []int{200}}) + return +} + +// Create implements create image request. +func CreateImageByOBS(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r JobResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(createURL(client), b, &r.Body, &golangsdk.RequestOpts{OkCodes: []int{200}}) + return +} + +// Create implements create image request. +func CreateDataImageByServer(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r JobResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(createURL(client), b, &r.Body, &golangsdk.RequestOpts{OkCodes: []int{200}}) + return +} + +// Create implements create image request. +func CreateDataImageByOBS(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r JobResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(createDataImageURL(client), b, &r.Body, &golangsdk.RequestOpts{OkCodes: []int{200}}) + return +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results.go new file mode 100644 index 0000000000..6c4bd96718 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results.go @@ -0,0 +1,157 @@ +package cloudimages + +import ( + "encoding/json" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// Image represents an image found in the IMS. +type Image struct { + // the URL for uploading and downloading the image file + File string `json:"file"` + // the image owner + Owner string `json:"owner"` + // the image id + ID string `json:"id"` + // the image URL + Self string `json:"self"` + // the image schema + Schema string `json:"schema"` + // the image status, the value can be [queued, saving, deleted, killed,active] + Status string `json:"status"` + // the image tags + Tags []string `json:"tags"` + // whether the image can be seen by others + Visibility string `json:"visibility"` + // the image name + Name string `json:"name"` + // whether the image has been deleted + Deleted bool `json:"deleted"` + // whether the image is protected + Protected bool `json:"protected"` + // the container type + ContainerFormat string `json:"container_format"` + // the minimum memory size (MB) required for running the image + MinRam int `json:"min_ram"` + // the maximum memory of the image in the unit of MB, notice: string + MaxRam string `json:"max_ram"` + // the disk format, the value can be [vhd, raw, zvhd, qcow2] + DiskFormat string `json:"disk_format"` + // the minimum disk space (GB) required for running the image + MinDisk int `json:"min_disk"` + // the environment where the image is used + VirtualEnvType string `json:"virtual_env_type"` + // *size, virtual_size and checksum parameter are unavailable currently* + Size int64 `json:"size"` + VirtualSize int `json:"virtual_size"` + Checksum string `json:"checksum"` + // created_at and updated_at are in UTC format + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt string `json:"deleted_at"` + // the OS architecture: 32 or 64 + OsBit string `json:"__os_bit"` + OsVersion string `json:"__os_version"` + Description string `json:"__description"` + OsType string `json:"__os_type"` + Isregistered string `json:"__isregistered"` + Platform string `json:"__platform"` + ImageSourceType string `json:"__image_source_type"` + Imagetype string `json:"__imagetype"` + Originalimagename string `json:"__originalimagename"` + BackupID string `json:"__backup_id"` + Productcode string `json:"__productcode"` + ImageSize string `json:"__image_size"` + DataOrigin string `json:"__data_origin"` + SupportKvm string `json:"__support_kvm"` + SupportXen string `json:"__support_xen"` + SupportLargeMemory string `json:"__support_largememory"` + SupportDiskintensive string `json:"__support_diskintensive"` + SupportHighperformance string `json:"__support_highperformance"` + SupportXenGpuType string `json:"__support_xen_gpu_type"` + SupportKvmGpuType string `json:"__support_kvm_gpu_type"` + SupportXenHana string `json:"__support_xen_hana"` + SupportKvmInfiniband string `json:"__support_kvm_infiniband"` + SystemSupportMarket bool `json:"__system_support_market"` + RootOrigin string `json:"__root_origin"` + SequenceNum string `json:"__sequence_num"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + CreatedAt golangsdk.JSONRFC3339Milli `json:"created_at"` + UpdatedAt golangsdk.JSONRFC3339Milli `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +type commonResult struct { + golangsdk.Result +} + +// Extract will get the Image object out of the commonResult object. +func (r commonResult) Extract() (*Image, error) { + var s Image + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractInto converts our response data into a volume struct +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "images") +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.serviceURL, s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, +// producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results_job.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results_job.go new file mode 100644 index 0000000000..ad5e6ab192 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/results_job.go @@ -0,0 +1,92 @@ +package cloudimages + +import ( + "fmt" + + "github.com/huaweicloud/golangsdk" +) + +type JobResponse struct { + JobID string `json:"job_id"` +} + +type JobStatus struct { + Status string `json:"status"` + Entities JobEntity `json:"entities"` + JobID string `json:"job_id"` + JobType string `json:"job_type"` + BeginTime string `json:"begin_time"` + EndTime string `json:"end_time"` + ErrorCode string `json:"error_code"` + FailReason string `json:"fail_reason"` +} + +type JobEntity struct { + ImageID string `json:"image_id"` + DataImageID string `json:"__data_images"` +} + +type JobResult struct { + golangsdk.Result +} + +func (r JobResult) ExtractJobResponse() (*JobResponse, error) { + job := new(JobResponse) + err := r.ExtractInto(job) + return job, err +} + +func (r JobResult) ExtractJobStatus() (*JobStatus, error) { + job := new(JobStatus) + err := r.ExtractInto(job) + return job, err +} + +func WaitForJobSuccess(client *golangsdk.ServiceClient, secs int, jobID string) error { + jobClient := *client + // v1/{project_id}/jobs/{job_id} + jobClient.ResourceBase = jobClient.Endpoint + "v1/" + jobClient.ProjectID + "/" + return golangsdk.WaitFor(secs, func() (bool, error) { + job := new(JobStatus) + _, err := jobClient.Get(jobClient.ServiceURL("jobs", jobID), &job, nil) + if err != nil { + return false, err + } + + if job.Status == "SUCCESS" { + return true, nil + } + if job.Status == "FAIL" { + err = fmt.Errorf("Job failed with code %s: %s.\n", job.ErrorCode, job.FailReason) + return false, err + } + + return false, nil + }) +} + +func GetJobEntity(client *golangsdk.ServiceClient, jobId string, label string) (interface{}, error) { + if label != "image_id" && label != "__data_images" { + return nil, fmt.Errorf("Unsupported label %s in GetJobEntity.", label) + } + + jobClient := *client + // v1/{project_id}/jobs/{job_id} + jobClient.ResourceBase = jobClient.Endpoint + "v1/" + jobClient.ProjectID + "/" + job := new(JobStatus) + _, err := jobClient.Get(jobClient.ServiceURL("jobs", jobId), &job, nil) + if err != nil { + return nil, err + } + + if job.Status == "SUCCESS" { + if e := job.Entities.ImageID; e != "" { + return e, nil + } + if e := job.Entities.DataImageID; e != "" { + return e, nil + } + } + + return nil, fmt.Errorf("Unexpected conversion error in GetJobEntity.") +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/urls.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/urls.go new file mode 100644 index 0000000000..31efa73346 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages/urls.go @@ -0,0 +1,46 @@ +package cloudimages + +import ( + "net/url" + "strings" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/utils" +) + +// query images using search criteria and to display the images in a list +func listURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("cloudimages") +} + +func createURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("cloudimages/action") +} + +func createDataImageURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("cloudimages/dataimages/action") +} + +// builds next page full url based on current url +func nextPageURL(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = golangsdk.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + return nextURL.String(), nil +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/requests.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/requests.go new file mode 100644 index 0000000000..c0028a435c --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/requests.go @@ -0,0 +1,61 @@ +package tags + +import ( + "github.com/huaweicloud/golangsdk" +) + +// Tag is a structure of key value pair. +type Tag struct { + //tag key + Key string `json:"key" required:"true"` + //tag value + Value string `json:"value" required:"true"` +} + +// BatchOptsBuilder allows extensions to add additional parameters to the +// BatchAction request. +type BatchOptsBuilder interface { + ToTagsBatchMap() (map[string]interface{}, error) +} + +// BatchOpts contains all the values needed to perform BatchAction on the image tags. +type BatchOpts struct { + //List of tags to perform batch operation + Tags []Tag `json:"tags,omitempty"` + //Operator , Possible values are:create or delete + Action ActionType `json:"action" required:"true"` +} + +//ActionType specifies the type of batch operation action to be performed +type ActionType string + +var ( + // ActionCreate is used to set action operator to create + ActionCreate ActionType = "create" + // ActionDelete is used to set action operator to delete + ActionDelete ActionType = "delete" +) + +// ToTagsBatchMap builds a BatchAction request body from BatchOpts. +func (opts BatchOpts) ToTagsBatchMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +//BatchAction is used to create ,update or delete the tags of a specified image. +func BatchAction(client *golangsdk.ServiceClient, imageID string, opts BatchOptsBuilder) (r ActionResults) { + b, err := opts.ToTagsBatchMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, imageID), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{204}, + }) + return +} + +// Get retrieves the tags of a specific image. +func Get(client *golangsdk.ServiceClient, imageID string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, imageID), &r.Body, nil) + return +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/results.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/results.go new file mode 100644 index 0000000000..206e2ce2b5 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/results.go @@ -0,0 +1,28 @@ +package tags + +import ( + "github.com/huaweicloud/golangsdk" +) + +type RespTags struct { + //contains list of tags, i.e.key value pair + Tags []Tag `json:"tags"` +} + +type commonResult struct { + golangsdk.Result +} + +type ActionResults struct { + commonResult +} + +type GetResult struct { + commonResult +} + +func (r commonResult) Extract() (*RespTags, error) { + var response RespTags + err := r.ExtractInto(&response) + return &response, err +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/urls.go b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/urls.go new file mode 100644 index 0000000000..53488a00b3 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/ims/v2/tags/urls.go @@ -0,0 +1,17 @@ +package tags + +import "github.com/huaweicloud/golangsdk" + +const ( + rootPath = "images" + resourcePath = "tags" + actionPath = "tags/action" +) + +func actionURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(c.ProjectID, rootPath, id, actionPath) +} + +func resourceURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(c.ProjectID, rootPath, id, resourcePath) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e4f722d63f..6ebad86e68 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -267,6 +267,8 @@ github.com/huaweicloud/golangsdk/openstack/identity/v3/tokens github.com/huaweicloud/golangsdk/openstack/identity/v3/users github.com/huaweicloud/golangsdk/openstack/imageservice/v2/imagedata github.com/huaweicloud/golangsdk/openstack/imageservice/v2/images +github.com/huaweicloud/golangsdk/openstack/ims/v2/cloudimages +github.com/huaweicloud/golangsdk/openstack/ims/v2/tags github.com/huaweicloud/golangsdk/openstack/kms/v1/keys github.com/huaweicloud/golangsdk/openstack/lts/huawei/loggroups github.com/huaweicloud/golangsdk/openstack/lts/huawei/logstreams