From 0b62690effb762da58c47525e6348be93c060aaa Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 8 Nov 2024 11:06:10 +0200 Subject: [PATCH 1/6] Ignore Idea project files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 736b9797..1a536260 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/* packer-plugin-tencentcloud .docs crash.log +.idea/ \ No newline at end of file From 14465424022d46ac04fc444fe91083f0ae38130d Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 8 Nov 2024 15:43:57 +0200 Subject: [PATCH 2/6] Remove unused ctx parameter --- builder/tencentcloud/cvm/access_config.go | 3 +-- builder/tencentcloud/cvm/access_config_test.go | 8 ++++---- builder/tencentcloud/cvm/builder.go | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/builder/tencentcloud/cvm/access_config.go b/builder/tencentcloud/cvm/access_config.go index 66a376d9..827192fe 100644 --- a/builder/tencentcloud/cvm/access_config.go +++ b/builder/tencentcloud/cvm/access_config.go @@ -16,7 +16,6 @@ import ( "strconv" "strings" - "github.com/hashicorp/packer-plugin-sdk/template/interpolate" "github.com/mitchellh/go-homedir" cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" @@ -174,7 +173,7 @@ func (cf *TencentCloudAccessConfig) Client() (*cvm.Client, *vpc.Client, error) { return nil, nil, fmt.Errorf("unknown zone: %s", cf.Zone) } -func (cf *TencentCloudAccessConfig) Prepare(ctx *interpolate.Context) []error { +func (cf *TencentCloudAccessConfig) Prepare() []error { var errs []error if err := cf.Config(); err != nil { diff --git a/builder/tencentcloud/cvm/access_config_test.go b/builder/tencentcloud/cvm/access_config_test.go index 61d68fff..71b9e24d 100644 --- a/builder/tencentcloud/cvm/access_config_test.go +++ b/builder/tencentcloud/cvm/access_config_test.go @@ -13,22 +13,22 @@ func TestTencentCloudAccessConfig_Prepare(t *testing.T) { SecretKey: "secret-key", } - if err := cf.Prepare(nil); err == nil { + if err := cf.Prepare(); err == nil { t.Fatal("should raise error: region not set") } cf.Region = "ap-guangzhou" - if err := cf.Prepare(nil); err != nil { + if err := cf.Prepare(); err != nil { t.Fatalf("shouldn't raise error: %v", err) } cf.Region = "unknown-region" - if err := cf.Prepare(nil); err == nil { + if err := cf.Prepare(); err == nil { t.Fatal("should raise error: unknown region") } cf.skipValidation = true - if err := cf.Prepare(nil); err != nil { + if err := cf.Prepare(); err != nil { t.Fatalf("shouldn't raise error: %v", err) } } diff --git a/builder/tencentcloud/cvm/builder.go b/builder/tencentcloud/cvm/builder.go index cda74a0c..22d8049c 100644 --- a/builder/tencentcloud/cvm/builder.go +++ b/builder/tencentcloud/cvm/builder.go @@ -62,7 +62,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { // Accumulate any errors var errs *packersdk.MultiError - errs = packersdk.MultiErrorAppend(errs, b.config.TencentCloudAccessConfig.Prepare(&b.config.ctx)...) + errs = packersdk.MultiErrorAppend(errs, b.config.TencentCloudAccessConfig.Prepare()...) errs = packersdk.MultiErrorAppend(errs, b.config.TencentCloudImageConfig.Prepare(&b.config.ctx)...) errs = packersdk.MultiErrorAppend(errs, b.config.TencentCloudRunConfig.Prepare(&b.config.ctx)...) if errs != nil && len(errs.Errors) > 0 { From 641d3cf0957049b31c3c311a7007a3479376e71b Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 8 Nov 2024 16:02:22 +0200 Subject: [PATCH 3/6] Add a "tencentcloud-image" data source for querying images Fixes #68 --- datasource/tencentcloud/image/data.go | 119 ++++++++++++++++++ .../tencentcloud/image/data.hcl2spec.go | 95 ++++++++++++++ .../tencentcloud/image/data_acc_test.go | 66 ++++++++++ datasource/tencentcloud/image/sorting.go | 29 +++++ .../image/test-fixtures/template.pkr.hcl | 33 +++++ .../tencentcloud/image/DatasourceOutput.mdx | 7 ++ .../image/ImageFilterOptions-not-required.mdx | 10 ++ main.go | 2 + 8 files changed, 361 insertions(+) create mode 100644 datasource/tencentcloud/image/data.go create mode 100644 datasource/tencentcloud/image/data.hcl2spec.go create mode 100644 datasource/tencentcloud/image/data_acc_test.go create mode 100644 datasource/tencentcloud/image/sorting.go create mode 100644 datasource/tencentcloud/image/test-fixtures/template.pkr.hcl create mode 100644 docs-partials/datasource/tencentcloud/image/DatasourceOutput.mdx create mode 100644 docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx diff --git a/datasource/tencentcloud/image/data.go b/datasource/tencentcloud/image/data.go new file mode 100644 index 00000000..57e688ff --- /dev/null +++ b/datasource/tencentcloud/image/data.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//go:generate packer-sdc struct-markdown +//go:generate packer-sdc mapstructure-to-hcl2 -type Config,DatasourceOutput +package image + +import ( + "fmt" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-sdk/common" + "github.com/hashicorp/packer-plugin-sdk/hcl2helper" + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer-plugin-sdk/template/config" + builder "github.com/hashicorp/packer-plugin-tencentcloud/builder/tencentcloud/cvm" + cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" + "github.com/zclconf/go-cty/cty" +) + +type ImageFilterOptions struct { + // Filters used to select an image. Any filter described in the documentation for + //[DescribeImages](https://www.tencentcloud.com/document/product/213/33272) can be used. + Filters map[string]string `mapstructure:"filters"` + // Selects the most recently created image when multiple results are returned. Note that + // public images don't have a creation date, so this flag is only really useful for private + // images. + MostRecent bool `mapstructure:"most_recent"` +} + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + builder.TencentCloudAccessConfig `mapstructure:",squash"` + ImageFilterOptions `mapstructure:",squash"` +} + +type Datasource struct { + config Config +} + +type DatasourceOutput struct { + // The image ID + ID string `mapstructure:"id"` + // The image name + Name string `mapstructure:"name"` +} + +func (d *Datasource) ConfigSpec() hcldec.ObjectSpec { + return d.config.FlatMapstructure().HCL2Spec() +} + +func (d *Datasource) Configure(raws ...interface{}) error { + err := config.Decode(&d.config, nil, raws...) + if err != nil { + return err + } + + var errs *packersdk.MultiError + errs = packersdk.MultiErrorAppend(errs, d.config.TencentCloudAccessConfig.Prepare()...) + + if len(d.config.Filters) == 0 { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`filters` must be specified")) + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + return nil +} + +func (d *Datasource) OutputSpec() hcldec.ObjectSpec { + return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec() +} + +func (d *Datasource) Execute() (cty.Value, error) { + client, _, err := d.config.Client() + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + + req := cvm.NewDescribeImagesRequest() + + var filters []*cvm.Filter + for k, v := range d.config.Filters { + k := k + v := v + filters = append(filters, &cvm.Filter{ + Name: &k, + Values: []*string{&v}, + }) + } + req.Filters = filters + + resp, err := client.DescribeImages(req) + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + + if *resp.Response.TotalCount == 0 { + return cty.NullVal(cty.EmptyObject), nil + } + + if *resp.Response.TotalCount > 1 && !d.config.MostRecent { + return cty.NullVal(cty.EmptyObject), fmt.Errorf("Your image query returned more than result. Please try a more specific search, or set `most_recent` to `true`.") + } + + var image *cvm.Image + + if d.config.MostRecent { + image = mostRecentImage(resp.Response.ImageSet) + } else { + image = resp.Response.ImageSet[0] + } + + output := DatasourceOutput{ + ID: *image.ImageId, + Name: *image.ImageName, + } + return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil +} diff --git a/datasource/tencentcloud/image/data.hcl2spec.go b/datasource/tencentcloud/image/data.hcl2spec.go new file mode 100644 index 00000000..c840e1bb --- /dev/null +++ b/datasource/tencentcloud/image/data.hcl2spec.go @@ -0,0 +1,95 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package image + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-tencentcloud/builder/tencentcloud/cvm" + "github.com/zclconf/go-cty/cty" +) + +// FlatConfig is an auto-generated flat version of Config. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatConfig struct { + PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` + PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` + PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"` + PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` + PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` + PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` + PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` + PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` + SecretId *string `mapstructure:"secret_id" required:"true" cty:"secret_id" hcl:"secret_id"` + SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"` + Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"` + Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"` + CvmEndpoint *string `mapstructure:"cvm_endpoint" required:"false" cty:"cvm_endpoint" hcl:"cvm_endpoint"` + VpcEndpoint *string `mapstructure:"vpc_endpoint" required:"false" cty:"vpc_endpoint" hcl:"vpc_endpoint"` + SecurityToken *string `mapstructure:"security_token" required:"false" cty:"security_token" hcl:"security_token"` + AssumeRole *cvm.FlatTencentCloudAccessRole `mapstructure:"assume_role" required:"false" cty:"assume_role" hcl:"assume_role"` + Profile *string `mapstructure:"profile" required:"false" cty:"profile" hcl:"profile"` + SharedCredentialsDir *string `mapstructure:"shared_credentials_dir" required:"false" cty:"shared_credentials_dir" hcl:"shared_credentials_dir"` + Filters map[string]string `mapstructure:"filters" cty:"filters" hcl:"filters"` + MostRecent *bool `mapstructure:"most_recent" cty:"most_recent" hcl:"most_recent"` +} + +// FlatMapstructure returns a new FlatConfig. +// FlatConfig is an auto-generated flat version of Config. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatConfig) +} + +// HCL2Spec returns the hcl spec of a Config. +// This spec is used by HCL to read the fields of Config. +// The decoded values from this spec will then be applied to a FlatConfig. +func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, + "secret_id": &hcldec.AttrSpec{Name: "secret_id", Type: cty.String, Required: false}, + "secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false}, + "region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false}, + "zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false}, + "cvm_endpoint": &hcldec.AttrSpec{Name: "cvm_endpoint", Type: cty.String, Required: false}, + "vpc_endpoint": &hcldec.AttrSpec{Name: "vpc_endpoint", Type: cty.String, Required: false}, + "security_token": &hcldec.AttrSpec{Name: "security_token", Type: cty.String, Required: false}, + "assume_role": &hcldec.BlockSpec{TypeName: "assume_role", Nested: hcldec.ObjectSpec((*cvm.FlatTencentCloudAccessRole)(nil).HCL2Spec())}, + "profile": &hcldec.AttrSpec{Name: "profile", Type: cty.String, Required: false}, + "shared_credentials_dir": &hcldec.AttrSpec{Name: "shared_credentials_dir", Type: cty.String, Required: false}, + "filters": &hcldec.AttrSpec{Name: "filters", Type: cty.Map(cty.String), Required: false}, + "most_recent": &hcldec.AttrSpec{Name: "most_recent", Type: cty.Bool, Required: false}, + } + return s +} + +// FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatDatasourceOutput struct { + ID *string `mapstructure:"id" cty:"id" hcl:"id"` + Name *string `mapstructure:"name" cty:"name" hcl:"name"` +} + +// FlatMapstructure returns a new FlatDatasourceOutput. +// FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*DatasourceOutput) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatDatasourceOutput) +} + +// HCL2Spec returns the hcl spec of a DatasourceOutput. +// This spec is used by HCL to read the fields of DatasourceOutput. +// The decoded values from this spec will then be applied to a FlatDatasourceOutput. +func (*FlatDatasourceOutput) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "id": &hcldec.AttrSpec{Name: "id", Type: cty.String, Required: false}, + "name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false}, + } + return s +} diff --git a/datasource/tencentcloud/image/data_acc_test.go b/datasource/tencentcloud/image/data_acc_test.go new file mode 100644 index 00000000..9a183b69 --- /dev/null +++ b/datasource/tencentcloud/image/data_acc_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package image + +import ( + _ "embed" + "fmt" + "io/ioutil" + "os" + "os/exec" + "regexp" + "testing" + + "github.com/hashicorp/packer-plugin-sdk/acctest" +) + +//go:embed test-fixtures/template.pkr.hcl +var testDatasourceHCL2Basic string + +// Run with: PACKER_ACC=1 go test -count 1 -v ./datasource/image/data_acc_test.go -timeout=120m +func TestAccImageDatasource(t *testing.T) { + testCase := &acctest.PluginTestCase{ + Name: "image_datasource_basic_test", + Setup: func() error { + return nil + }, + Teardown: func() error { + return nil + }, + Template: testDatasourceHCL2Basic, + Type: "image-my-datasource", + Check: func(buildCommand *exec.Cmd, logfile string) error { + if buildCommand.ProcessState != nil { + if buildCommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + + logs, err := os.Open(logfile) + if err != nil { + return fmt.Errorf("Unable find %s", logfile) + } + defer logs.Close() + + logsBytes, err := ioutil.ReadAll(logs) + if err != nil { + return fmt.Errorf("Unable to read %s", logfile) + } + logsString := string(logsBytes) + + idLog := "null.basic-example: id: img-39ei7bw5" + nameLog := "null.basic-example: name: Rocky Linux 9.4 64bit" + + if matched, _ := regexp.MatchString(idLog+".*", logsString); !matched { + t.Fatalf("logs doesn't contain expected ID value %q", logsString) + } + if matched, _ := regexp.MatchString(nameLog+".*", logsString); !matched { + t.Fatalf("logs doesn't contain expected name value %q", logsString) + } + + return nil + }, + } + acctest.TestPlugin(t, testCase) +} diff --git a/datasource/tencentcloud/image/sorting.go b/datasource/tencentcloud/image/sorting.go new file mode 100644 index 00000000..4da04ea5 --- /dev/null +++ b/datasource/tencentcloud/image/sorting.go @@ -0,0 +1,29 @@ +package image + +import ( + cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" + "sort" + "time" +) + +type imageSort []*cvm.Image + +func (a imageSort) Len() int { return len(a) } +func (a imageSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a imageSort) Less(i, j int) bool { + // Public images don't have a creation time + if a[i].CreatedTime == nil || a[j].CreatedTime == nil { + return false + } + + itime, _ := time.Parse(time.RFC3339, *a[i].CreatedTime) + jtime, _ := time.Parse(time.RFC3339, *a[j].CreatedTime) + return itime.Before(jtime) +} + +func mostRecentImage(images []*cvm.Image) *cvm.Image { + sortedImages := images + sort.Sort(imageSort(sortedImages)) + + return sortedImages[len(sortedImages)-1] +} diff --git a/datasource/tencentcloud/image/test-fixtures/template.pkr.hcl b/datasource/tencentcloud/image/test-fixtures/template.pkr.hcl new file mode 100644 index 00000000..405c96ba --- /dev/null +++ b/datasource/tencentcloud/image/test-fixtures/template.pkr.hcl @@ -0,0 +1,33 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +data "tencentcloud-image" "test-image" { + filters = { + image-type = "PUBLIC_IMAGE" + platform = "Rocky Linux" + image-name = "Rocky Linux 9.4 64bit" + } + most_recent = true +} + +locals { + id = data.tencentcloud-image.test-image.id + name = data.tencentcloud-image.test-image.name +} + +source "null" "basic-example" { + communicator = "none" +} + +build { + sources = [ + "source.null.basic-example" + ] + + provisioner "shell-local" { + inline = [ + "echo id: ${local.id}", + "echo name: ${local.name}", + ] + } +} diff --git a/docs-partials/datasource/tencentcloud/image/DatasourceOutput.mdx b/docs-partials/datasource/tencentcloud/image/DatasourceOutput.mdx new file mode 100644 index 00000000..1e829daa --- /dev/null +++ b/docs-partials/datasource/tencentcloud/image/DatasourceOutput.mdx @@ -0,0 +1,7 @@ + + +- `id` (string) - The image ID + +- `name` (string) - The image name + + diff --git a/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx b/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx new file mode 100644 index 00000000..e0716b33 --- /dev/null +++ b/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx @@ -0,0 +1,10 @@ + + +- `filters` (map[string]string) - Filters used to select an image. Any filter described in the documentation for + [DescribeImages](https://www.tencentcloud.com/document/product/213/33272) can be used. + +- `most_recent` (bool) - Selects the most recently created image when multiple results are returned. Note that + public images don't have a creation date, so this flag is only really useful for private + images. + + diff --git a/main.go b/main.go index 212f8fc5..3267a4e7 100644 --- a/main.go +++ b/main.go @@ -10,12 +10,14 @@ import ( "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer-plugin-tencentcloud/builder/tencentcloud/cvm" + "github.com/hashicorp/packer-plugin-tencentcloud/datasource/tencentcloud/image" "github.com/hashicorp/packer-plugin-tencentcloud/version" ) func main() { pps := plugin.NewSet() pps.RegisterBuilder("cvm", new(cvm.Builder)) + pps.RegisterDatasource("image", new(image.Datasource)) pps.SetVersion(version.PluginVersion) err := pps.Run() if err != nil { From d9cd1ab34f521cb99872a57544ae888e3b198106 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 11 Nov 2024 10:04:11 +0200 Subject: [PATCH 4/6] Remove too strict source_image_id validation The value is "" when running "packer validate" with a datasource --- builder/tencentcloud/cvm/common.go | 7 ------- builder/tencentcloud/cvm/run_config.go | 4 ---- 2 files changed, 11 deletions(-) diff --git a/builder/tencentcloud/cvm/common.go b/builder/tencentcloud/cvm/common.go index 13ae9cb2..7c9bafef 100644 --- a/builder/tencentcloud/cvm/common.go +++ b/builder/tencentcloud/cvm/common.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "net/url" - "regexp" "strings" "time" @@ -143,12 +142,6 @@ func NewVpcClient(cf *TencentCloudAccessConfig) (client *vpc.Client, err error) return } -// CheckResourceIdFormat check resource id format -func CheckResourceIdFormat(resource string, id string) bool { - regex := regexp.MustCompile(fmt.Sprintf("%s-[0-9a-z]{8}$", resource)) - return regex.MatchString(id) -} - // SSHHost returns a function that can be given to the SSH communicator func SSHHost(pubilcIp bool) func(multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) { diff --git a/builder/tencentcloud/cvm/run_config.go b/builder/tencentcloud/cvm/run_config.go index e4a45e11..0cdc04f8 100644 --- a/builder/tencentcloud/cvm/run_config.go +++ b/builder/tencentcloud/cvm/run_config.go @@ -126,10 +126,6 @@ func (cf *TencentCloudRunConfig) Prepare(ctx *interpolate.Context) []error { errs = append(errs, errors.New("source_image_id or source_image_name must be specified")) } - if cf.SourceImageId != "" && !CheckResourceIdFormat("img", cf.SourceImageId) { - errs = append(errs, errors.New("source_image_id wrong format")) - } - if cf.InstanceType == "" { errs = append(errs, errors.New("instance_type must be specified")) } From 793b4c62bff50aab0fe5acb36ac1a4c63301107f Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 11 Nov 2024 11:23:49 +0200 Subject: [PATCH 5/6] Add support for finding images by image family Related to https://github.com/hashicorp/packer-plugin-tencentcloud/pull/138 --- datasource/tencentcloud/image/data.go | 71 +++++++++++---- .../tencentcloud/image/data.hcl2spec.go | 2 + datasource/tencentcloud/image/data_test.go | 88 +++++++++++++++++++ .../image/ImageFilterOptions-not-required.mdx | 4 + 4 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 datasource/tencentcloud/image/data_test.go diff --git a/datasource/tencentcloud/image/data.go b/datasource/tencentcloud/image/data.go index 57e688ff..e5b58e39 100644 --- a/datasource/tencentcloud/image/data.go +++ b/datasource/tencentcloud/image/data.go @@ -19,8 +19,12 @@ import ( type ImageFilterOptions struct { // Filters used to select an image. Any filter described in the documentation for - //[DescribeImages](https://www.tencentcloud.com/document/product/213/33272) can be used. + // [DescribeImages](https://www.tencentcloud.com/document/product/213/33272) can be used. Filters map[string]string `mapstructure:"filters"` + // Image family used to select an image. Uses the + // [DescribeImageFromFamily](https://www.tencentcloud.com/document/product/213/64971) API. + // Mutually exclusive with `filters`, and `most_recent` will have no effect. + ImageFamily string `mapstructure:"image_family"` // Selects the most recently created image when multiple results are returned. Note that // public images don't have a creation date, so this flag is only really useful for private // images. @@ -57,8 +61,12 @@ func (d *Datasource) Configure(raws ...interface{}) error { var errs *packersdk.MultiError errs = packersdk.MultiErrorAppend(errs, d.config.TencentCloudAccessConfig.Prepare()...) - if len(d.config.Filters) == 0 { - errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`filters` must be specified")) + if len(d.config.Filters) == 0 && d.config.ImageFamily == "" { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`filters` or `image_family` must be specified")) + } + + if len(d.config.Filters) > 0 && d.config.ImageFamily != "" { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`filters` and `image_family` are mutually exclusive")) } if errs != nil && len(errs.Errors) > 0 { @@ -72,11 +80,32 @@ func (d *Datasource) OutputSpec() hcldec.ObjectSpec { } func (d *Datasource) Execute() (cty.Value, error) { - client, _, err := d.config.Client() + var image *cvm.Image + var err error + + if len(d.config.Filters) > 0 { + image, err = d.ResolveImageByFilters() + } else { + image, err = d.ResolveImageByImageFamily() + } + if err != nil { return cty.NullVal(cty.EmptyObject), err } + output := DatasourceOutput{ + ID: *image.ImageId, + Name: *image.ImageName, + } + return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil +} + +func (d *Datasource) ResolveImageByFilters() (*cvm.Image, error) { + client, _, err := d.config.Client() + if err != nil { + return nil, err + } + req := cvm.NewDescribeImagesRequest() var filters []*cvm.Filter @@ -92,28 +121,40 @@ func (d *Datasource) Execute() (cty.Value, error) { resp, err := client.DescribeImages(req) if err != nil { - return cty.NullVal(cty.EmptyObject), err + return nil, err } if *resp.Response.TotalCount == 0 { - return cty.NullVal(cty.EmptyObject), nil + return nil, fmt.Errorf("No image found using the specified filters") } if *resp.Response.TotalCount > 1 && !d.config.MostRecent { - return cty.NullVal(cty.EmptyObject), fmt.Errorf("Your image query returned more than result. Please try a more specific search, or set `most_recent` to `true`.") + return nil, fmt.Errorf("Your image query returned more than result. Please try a more specific search, or set `most_recent` to `true`.") } - var image *cvm.Image - if d.config.MostRecent { - image = mostRecentImage(resp.Response.ImageSet) + return mostRecentImage(resp.Response.ImageSet), nil } else { - image = resp.Response.ImageSet[0] + return resp.Response.ImageSet[0], nil } +} - output := DatasourceOutput{ - ID: *image.ImageId, - Name: *image.ImageName, +func (d *Datasource) ResolveImageByImageFamily() (*cvm.Image, error) { + client, _, err := d.config.Client() + if err != nil { + return nil, err + } + + req := cvm.NewDescribeImageFromFamilyRequest() + req.ImageFamily = &d.config.ImageFamily + + resp, err := client.DescribeImageFromFamily(req) + + if err != nil { + return nil, err + } else if resp.Response.Image == nil { + return nil, fmt.Errorf("No image found using the specified image family") + } else { + return resp.Response.Image, nil } - return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil } diff --git a/datasource/tencentcloud/image/data.hcl2spec.go b/datasource/tencentcloud/image/data.hcl2spec.go index c840e1bb..6a16c694 100644 --- a/datasource/tencentcloud/image/data.hcl2spec.go +++ b/datasource/tencentcloud/image/data.hcl2spec.go @@ -30,6 +30,7 @@ type FlatConfig struct { Profile *string `mapstructure:"profile" required:"false" cty:"profile" hcl:"profile"` SharedCredentialsDir *string `mapstructure:"shared_credentials_dir" required:"false" cty:"shared_credentials_dir" hcl:"shared_credentials_dir"` Filters map[string]string `mapstructure:"filters" cty:"filters" hcl:"filters"` + ImageFamily *string `mapstructure:"image_family" cty:"image_family" hcl:"image_family"` MostRecent *bool `mapstructure:"most_recent" cty:"most_recent" hcl:"most_recent"` } @@ -64,6 +65,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "profile": &hcldec.AttrSpec{Name: "profile", Type: cty.String, Required: false}, "shared_credentials_dir": &hcldec.AttrSpec{Name: "shared_credentials_dir", Type: cty.String, Required: false}, "filters": &hcldec.AttrSpec{Name: "filters", Type: cty.Map(cty.String), Required: false}, + "image_family": &hcldec.AttrSpec{Name: "image_family", Type: cty.String, Required: false}, "most_recent": &hcldec.AttrSpec{Name: "most_recent", Type: cty.Bool, Required: false}, } return s diff --git a/datasource/tencentcloud/image/data_test.go b/datasource/tencentcloud/image/data_test.go new file mode 100644 index 00000000..8c6c5608 --- /dev/null +++ b/datasource/tencentcloud/image/data_test.go @@ -0,0 +1,88 @@ +package image + +import ( + cvm "github.com/hashicorp/packer-plugin-tencentcloud/builder/tencentcloud/cvm" + "testing" +) + +var tencentCloudAccessConfig = cvm.TencentCloudAccessConfig{ + Region: "na-ashburn", + SecretId: "secret", + SecretKey: "key", +} + +func TestDatasourceConfigure_NoOptionsSpecified(t *testing.T) { + ds := Datasource{ + config: Config{ + TencentCloudAccessConfig: tencentCloudAccessConfig, + ImageFilterOptions: ImageFilterOptions{ + Filters: map[string]string{}, + ImageFamily: "", + MostRecent: false, + }, + }, + } + + if err := ds.Configure(); err == nil { + t.Fatal("Should fail since at least one option must be specified") + } else { + t.Log(err) + } +} + +func TestDatasourceConfigure_BothFiltersAndImageFamilySpecified(t *testing.T) { + ds := Datasource{ + config: Config{ + TencentCloudAccessConfig: tencentCloudAccessConfig, + ImageFilterOptions: ImageFilterOptions{ + Filters: map[string]string{ + "foo": "bar", + }, + ImageFamily: "foo", + MostRecent: false, + }, + }, + } + + if err := ds.Configure(); err == nil { + t.Fatal("Should fail since options are mutually exclusive") + } else { + t.Log(err) + } +} + +func TestDatasourceConfigure_FiltersSpecified(t *testing.T) { + ds := Datasource{ + config: Config{ + TencentCloudAccessConfig: tencentCloudAccessConfig, + ImageFilterOptions: ImageFilterOptions{ + Filters: map[string]string{ + "foo": "bar", + }, + ImageFamily: "", + MostRecent: false, + }, + }, + } + + if err := ds.Configure(); err != nil { + t.Fatal("Should not fail") + } +} + +func TestDatasourceConfigure_ImageFamilySpecified(t *testing.T) { + ds := Datasource{ + config: Config{ + TencentCloudAccessConfig: tencentCloudAccessConfig, + ImageFilterOptions: ImageFilterOptions{ + Filters: map[string]string{}, + ImageFamily: "foo", + MostRecent: false, + }, + }, + } + + if err := ds.Configure(); err != nil { + t.Fatal("Should not fail") + } +} diff --git a/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx b/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx index e0716b33..42c8c7d3 100644 --- a/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx +++ b/docs-partials/datasource/tencentcloud/image/ImageFilterOptions-not-required.mdx @@ -3,6 +3,10 @@ - `filters` (map[string]string) - Filters used to select an image. Any filter described in the documentation for [DescribeImages](https://www.tencentcloud.com/document/product/213/33272) can be used. +- `image_family` (string) - Image family used to select an image. Uses the + [DescribeImageFromFamily](https://www.tencentcloud.com/document/product/213/64971) API. + Mutually exclusive with `filters`, and `most_recent` will have no effect. + - `most_recent` (bool) - Selects the most recently created image when multiple results are returned. Note that public images don't have a creation date, so this flag is only really useful for private images. From 59e858a3d40206c3c5f534fedcae1a8f6d2d8332 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 21 Jan 2025 15:39:39 +0200 Subject: [PATCH 6/6] Move "zone" to run configuration instead of access Zone is only needed by the builder, not by the data source --- .web-docs/components/builder/cvm/README.md | 8 +++--- builder/tencentcloud/cvm/access_config.go | 28 +------------------ builder/tencentcloud/cvm/builder.hcl2spec.go | 4 +-- builder/tencentcloud/cvm/run_config.go | 4 +++ builder/tencentcloud/cvm/step_copy_image.go | 1 - .../tencentcloud/image/data.hcl2spec.go | 2 -- .../cvm/TencentCloudAccessConfig-required.mdx | 4 --- .../cvm/TencentCloudRunConfig-required.mdx | 4 +++ 8 files changed, 15 insertions(+), 40 deletions(-) diff --git a/.web-docs/components/builder/cvm/README.md b/.web-docs/components/builder/cvm/README.md index 227bbb61..6abf167c 100644 --- a/.web-docs/components/builder/cvm/README.md +++ b/.web-docs/components/builder/cvm/README.md @@ -24,10 +24,6 @@ a [communicator](/packer/docs/templates/legacy_json_templates/communicator) can reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) for parameter taking. -- `zone` (string) - The zone where your cvm will be launch. You should - reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) - for parameter taking. - @@ -37,6 +33,10 @@ a [communicator](/packer/docs/templates/legacy_json_templates/communicator) can You should reference [Instance Type](https://intl.cloud.tencent.com/document/product/213/11518) for parameter taking. +- `zone` (string) - The zone where your cvm will be launch. You should + reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) + for parameter taking. + diff --git a/builder/tencentcloud/cvm/access_config.go b/builder/tencentcloud/cvm/access_config.go index 827192fe..5039b2fb 100644 --- a/builder/tencentcloud/cvm/access_config.go +++ b/builder/tencentcloud/cvm/access_config.go @@ -7,7 +7,6 @@ package cvm import ( - "context" "encoding/json" "fmt" "io/ioutil" @@ -80,10 +79,6 @@ type TencentCloudAccessConfig struct { // reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) // for parameter taking. Region string `mapstructure:"region" required:"true"` - // The zone where your cvm will be launch. You should - // reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) - // for parameter taking. - Zone string `mapstructure:"zone" required:"true"` // The endpoint you want to reach the cloud endpoint, // if tce cloud you should set a tce cvm endpoint. CvmEndpoint string `mapstructure:"cvm_endpoint" required:"false"` @@ -135,17 +130,12 @@ func (cf *TencentCloudAccessConfig) Client() (*cvm.Client, *vpc.Client, error) { err error cvm_client *cvm.Client vpc_client *vpc.Client - resp *cvm.DescribeZonesResponse ) if err = cf.validateRegion(); err != nil { return nil, nil, err } - if cf.Zone == "" { - return nil, nil, fmt.Errorf("parameter zone must be set") - } - if cvm_client, err = NewCvmClient(cf); err != nil { return nil, nil, err } @@ -154,23 +144,7 @@ func (cf *TencentCloudAccessConfig) Client() (*cvm.Client, *vpc.Client, error) { return nil, nil, err } - ctx := context.TODO() - err = Retry(ctx, func(ctx context.Context) error { - var e error - resp, e = cvm_client.DescribeZones(nil) - return e - }) - if err != nil { - return nil, nil, err - } - - for _, zone := range resp.Response.ZoneSet { - if cf.Zone == *zone.Zone { - return cvm_client, vpc_client, nil - } - } - - return nil, nil, fmt.Errorf("unknown zone: %s", cf.Zone) + return cvm_client, vpc_client, nil } func (cf *TencentCloudAccessConfig) Prepare() []error { diff --git a/builder/tencentcloud/cvm/builder.hcl2spec.go b/builder/tencentcloud/cvm/builder.hcl2spec.go index 51eadf70..2d3aa9d6 100644 --- a/builder/tencentcloud/cvm/builder.hcl2spec.go +++ b/builder/tencentcloud/cvm/builder.hcl2spec.go @@ -22,7 +22,6 @@ type FlatConfig struct { SecretId *string `mapstructure:"secret_id" required:"true" cty:"secret_id" hcl:"secret_id"` SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"` Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"` - Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"` CvmEndpoint *string `mapstructure:"cvm_endpoint" required:"false" cty:"cvm_endpoint" hcl:"cvm_endpoint"` VpcEndpoint *string `mapstructure:"vpc_endpoint" required:"false" cty:"vpc_endpoint" hcl:"vpc_endpoint"` SecurityToken *string `mapstructure:"security_token" required:"false" cty:"security_token" hcl:"security_token"` @@ -47,6 +46,7 @@ type FlatConfig struct { DataDisks []FlattencentCloudDataDisk `mapstructure:"data_disks" cty:"data_disks" hcl:"data_disks"` VpcId *string `mapstructure:"vpc_id" required:"false" cty:"vpc_id" hcl:"vpc_id"` VpcName *string `mapstructure:"vpc_name" required:"false" cty:"vpc_name" hcl:"vpc_name"` + Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"` SubnetId *string `mapstructure:"subnet_id" required:"false" cty:"subnet_id" hcl:"subnet_id"` SubnetName *string `mapstructure:"subnet_name" required:"false" cty:"subnet_name" hcl:"subnet_name"` CidrBlock *string `mapstructure:"cidr_block" required:"false" cty:"cidr_block" hcl:"cidr_block"` @@ -138,7 +138,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "secret_id": &hcldec.AttrSpec{Name: "secret_id", Type: cty.String, Required: false}, "secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false}, "region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false}, - "zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false}, "cvm_endpoint": &hcldec.AttrSpec{Name: "cvm_endpoint", Type: cty.String, Required: false}, "vpc_endpoint": &hcldec.AttrSpec{Name: "vpc_endpoint", Type: cty.String, Required: false}, "security_token": &hcldec.AttrSpec{Name: "security_token", Type: cty.String, Required: false}, @@ -163,6 +162,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "data_disks": &hcldec.BlockListSpec{TypeName: "data_disks", Nested: hcldec.ObjectSpec((*FlattencentCloudDataDisk)(nil).HCL2Spec())}, "vpc_id": &hcldec.AttrSpec{Name: "vpc_id", Type: cty.String, Required: false}, "vpc_name": &hcldec.AttrSpec{Name: "vpc_name", Type: cty.String, Required: false}, + "zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false}, "subnet_id": &hcldec.AttrSpec{Name: "subnet_id", Type: cty.String, Required: false}, "subnet_name": &hcldec.AttrSpec{Name: "subnet_name", Type: cty.String, Required: false}, "cidr_block": &hcldec.AttrSpec{Name: "cidr_block", Type: cty.String, Required: false}, diff --git a/builder/tencentcloud/cvm/run_config.go b/builder/tencentcloud/cvm/run_config.go index 0cdc04f8..625abbe7 100644 --- a/builder/tencentcloud/cvm/run_config.go +++ b/builder/tencentcloud/cvm/run_config.go @@ -66,6 +66,10 @@ type TencentCloudRunConfig struct { // Specify vpc name you will create. if `vpc_id` is not set, Packer will // create a vpc for you named this parameter. VpcName string `mapstructure:"vpc_name" required:"false"` + // The zone where your cvm will be launch. You should + // reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) + // for parameter taking. + Zone string `mapstructure:"zone" required:"true"` // Specify subnet your cvm will be launched by. SubnetId string `mapstructure:"subnet_id" required:"false"` // Specify subnet name you will create. if `subnet_id` is not set, Packer will diff --git a/builder/tencentcloud/cvm/step_copy_image.go b/builder/tencentcloud/cvm/step_copy_image.go index bc481e87..65af8626 100644 --- a/builder/tencentcloud/cvm/step_copy_image.go +++ b/builder/tencentcloud/cvm/step_copy_image.go @@ -54,7 +54,6 @@ func (s *stepCopyImage) Run(ctx context.Context, state multistep.StateBag) multi cf := &TencentCloudAccessConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, - Zone: config.Zone, CvmEndpoint: config.CvmEndpoint, SecurityToken: config.SecurityToken, AssumeRole: TencentCloudAccessRole{ diff --git a/datasource/tencentcloud/image/data.hcl2spec.go b/datasource/tencentcloud/image/data.hcl2spec.go index 6a16c694..4fd94010 100644 --- a/datasource/tencentcloud/image/data.hcl2spec.go +++ b/datasource/tencentcloud/image/data.hcl2spec.go @@ -22,7 +22,6 @@ type FlatConfig struct { SecretId *string `mapstructure:"secret_id" required:"true" cty:"secret_id" hcl:"secret_id"` SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"` Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"` - Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"` CvmEndpoint *string `mapstructure:"cvm_endpoint" required:"false" cty:"cvm_endpoint" hcl:"cvm_endpoint"` VpcEndpoint *string `mapstructure:"vpc_endpoint" required:"false" cty:"vpc_endpoint" hcl:"vpc_endpoint"` SecurityToken *string `mapstructure:"security_token" required:"false" cty:"security_token" hcl:"security_token"` @@ -57,7 +56,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "secret_id": &hcldec.AttrSpec{Name: "secret_id", Type: cty.String, Required: false}, "secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false}, "region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false}, - "zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false}, "cvm_endpoint": &hcldec.AttrSpec{Name: "cvm_endpoint", Type: cty.String, Required: false}, "vpc_endpoint": &hcldec.AttrSpec{Name: "vpc_endpoint", Type: cty.String, Required: false}, "security_token": &hcldec.AttrSpec{Name: "security_token", Type: cty.String, Required: false}, diff --git a/docs-partials/builder/tencentcloud/cvm/TencentCloudAccessConfig-required.mdx b/docs-partials/builder/tencentcloud/cvm/TencentCloudAccessConfig-required.mdx index 3f93b9a8..80aff723 100644 --- a/docs-partials/builder/tencentcloud/cvm/TencentCloudAccessConfig-required.mdx +++ b/docs-partials/builder/tencentcloud/cvm/TencentCloudAccessConfig-required.mdx @@ -10,8 +10,4 @@ reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) for parameter taking. -- `zone` (string) - The zone where your cvm will be launch. You should - reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) - for parameter taking. - diff --git a/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-required.mdx b/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-required.mdx index 7bc83eb7..dd2eaef3 100644 --- a/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-required.mdx +++ b/docs-partials/builder/tencentcloud/cvm/TencentCloudRunConfig-required.mdx @@ -4,4 +4,8 @@ You should reference [Instance Type](https://intl.cloud.tencent.com/document/product/213/11518) for parameter taking. +- `zone` (string) - The zone where your cvm will be launch. You should + reference [Region and Zone](https://intl.cloud.tencent.com/document/product/213/6091) + for parameter taking. +