Skip to content

Add a "tencentcloud-image" data source for querying images #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist/*
packer-plugin-tencentcloud
.docs
crash.log
.idea/
8 changes: 4 additions & 4 deletions .web-docs/components/builder/cvm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- End of code generated from the comments of the TencentCloudAccessConfig struct in builder/tencentcloud/cvm/access_config.go; -->


Expand All @@ -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.

<!-- End of code generated from the comments of the TencentCloudRunConfig struct in builder/tencentcloud/cvm/run_config.go; -->


Expand Down
31 changes: 2 additions & 29 deletions builder/tencentcloud/cvm/access_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
package cvm

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
Expand All @@ -16,7 +15,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"
Expand Down Expand Up @@ -81,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"`
Expand Down Expand Up @@ -136,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
}
Expand All @@ -155,26 +144,10 @@ 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(ctx *interpolate.Context) []error {
func (cf *TencentCloudAccessConfig) Prepare() []error {
var errs []error

if err := cf.Config(); err != nil {
Expand Down
8 changes: 4 additions & 4 deletions builder/tencentcloud/cvm/access_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion builder/tencentcloud/cvm/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions builder/tencentcloud/cvm/builder.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions builder/tencentcloud/cvm/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"context"
"fmt"
"net/url"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions builder/tencentcloud/cvm/run_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -126,10 +130,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"))
}
Expand Down
1 change: 0 additions & 1 deletion builder/tencentcloud/cvm/step_copy_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
160 changes: 160 additions & 0 deletions datasource/tencentcloud/image/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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"`
// 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.
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 && 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 {
return errs
}
return nil
}

func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
}

func (d *Datasource) Execute() (cty.Value, error) {
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
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 nil, err
}

if *resp.Response.TotalCount == 0 {
return nil, fmt.Errorf("No image found using the specified filters")
}

if *resp.Response.TotalCount > 1 && !d.config.MostRecent {
return nil, fmt.Errorf("Your image query returned more than result. Please try a more specific search, or set `most_recent` to `true`.")
}

if d.config.MostRecent {
return mostRecentImage(resp.Response.ImageSet), nil
} else {
return resp.Response.ImageSet[0], nil
}
}

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
}
}
Loading