packer-cn/post-processor/yandex-import/post-processor.go

275 lines
8.8 KiB
Go
Raw Normal View History

2020-07-08 15:54:20 -04:00
//go:generate struct-markdown
2020-07-08 14:58:56 -04:00
//go:generate mapstructure-to-hcl2 -type Config
package yandeximport
import (
"context"
"fmt"
"os"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/builder/file"
2020-07-21 18:01:30 -04:00
"github.com/hashicorp/packer/builder/yandex"
2020-07-08 14:58:56 -04:00
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/post-processor/artifice"
"github.com/hashicorp/packer/post-processor/compress"
2020-07-16 18:22:28 -04:00
yandexexport "github.com/hashicorp/packer/post-processor/yandex-export"
2020-07-08 14:58:56 -04:00
"github.com/hashicorp/packer/template/interpolate"
2020-07-21 18:01:30 -04:00
"github.com/yandex-cloud/go-genproto/yandex/cloud/iam/v1/awscompatibility"
"github.com/yandex-cloud/go-sdk/iamkey"
2020-07-08 14:58:56 -04:00
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// The folder ID that will be used to store imported Image.
FolderID string `mapstructure:"folder_id" required:"true"`
// Service Account ID with proper permission to use Storage service
// for operations 'upload' and 'delete' object to `bucket`
ServiceAccountID string `mapstructure:"service_account_id" required:"true"`
2020-07-08 17:24:53 -04:00
// OAuth token to use to authenticate to Yandex.Cloud.
2020-07-08 14:58:56 -04:00
Token string `mapstructure:"token" required:"false"`
// Path to file with Service Account key in json format. This
2020-07-08 17:24:53 -04:00
// is an alternative method to authenticate to Yandex.Cloud.
2020-07-08 14:58:56 -04:00
ServiceAccountKeyFile string `mapstructure:"service_account_key_file" required:"false"`
// The name of the bucket where the qcow2 file will be uploaded to for import.
2020-07-08 14:58:56 -04:00
// This bucket must exist when the post-processor is run.
//
// If import occurred after Yandex-Export post-processor, artifact already
// in storage service and first paths (URL) is used to, so no need to set this param.
Bucket string `mapstructure:"bucket" required:"false"`
// The name of the object key in `bucket` where the qcow2 file will be copied to import.
// This is a [template engine](/docs/templates/engine).
// Therefore, you may use user variables and template functions in this field.
2020-07-08 14:58:56 -04:00
ObjectName string `mapstructure:"object_name" required:"false"`
// Whether skip removing the qcow2 file uploaded to Storage
// after the import process has completed. Possible values are: `true` to
// leave it in the bucket, `false` to remove it. (Default: `false`).
SkipClean bool `mapstructure:"skip_clean" required:"false"`
// The name of the image, which contains 1-63 characters and only
// supports lowercase English characters, numbers and hyphen.
2020-07-08 16:01:25 -04:00
ImageName string `mapstructure:"image_name" required:"false"`
2020-07-08 14:58:56 -04:00
// The description of the image.
ImageDescription string `mapstructure:"image_description" required:"false"`
// The family name of the imported image.
2020-07-08 14:58:56 -04:00
ImageFamily string `mapstructure:"image_family" required:"false"`
// Key/value pair labels to apply to the imported image.
ImageLabels map[string]string `mapstructure:"image_labels" required:"false"`
ctx interpolate.Context
}
type PostProcessor struct {
config Config
}
func (p *PostProcessor) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
func (p *PostProcessor) Configure(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"object_name",
},
},
}, raws...)
if err != nil {
return err
}
errs := new(packer.MultiError)
// provision config by OS environment variables
if p.config.Token == "" {
p.config.Token = os.Getenv("YC_TOKEN")
}
if p.config.ServiceAccountKeyFile == "" {
p.config.ServiceAccountKeyFile = os.Getenv("YC_SERVICE_ACCOUNT_KEY_FILE")
}
if p.config.Token != "" {
packer.LogSecretFilter.Set(p.config.Token)
}
if p.config.ServiceAccountKeyFile != "" {
if _, err := iamkey.ReadFromJSONFile(p.config.ServiceAccountKeyFile); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("fail to read service account key file: %s", err))
}
}
if p.config.FolderID == "" {
p.config.FolderID = os.Getenv("YC_FOLDER_ID")
}
2020-07-08 14:58:56 -04:00
// Set defaults
if p.config.ObjectName == "" {
p.config.ObjectName = "packer-import-{{timestamp}}.qcow2"
}
// Check and render object_name
if err = interpolate.Validate(p.config.ObjectName, &p.config.ctx); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("error parsing object_name template: %s", err))
}
// TODO: make common code to check and prepare Yandex.Cloud auth configuration data
templates := map[string]*string{
"object_name": &p.config.ObjectName,
"folder_id": &p.config.FolderID,
}
for key, ptr := range templates {
if *ptr == "" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("%s must be set", key))
}
}
if len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) {
2020-07-21 18:01:30 -04:00
var imageSrc cloudImageSource
var fileSource bool
var err error
2020-07-08 14:58:56 -04:00
generatedData := artifact.State("generated_data")
if generatedData == nil {
// Make sure it's not a nil map so we can assign to it later.
generatedData = make(map[string]interface{})
}
p.config.ctx.Data = generatedData
2020-07-21 18:01:30 -04:00
p.config.ObjectName, err = interpolate.Render(p.config.ObjectName, &p.config.ctx)
if err != nil {
return nil, false, false, fmt.Errorf("error rendering object_name template: %s", err)
}
2020-07-08 14:58:56 -04:00
cfg := &yandex.Config{
Token: p.config.Token,
ServiceAccountKeyFile: p.config.ServiceAccountKeyFile,
}
client, err := yandex.NewDriverYC(ui, cfg)
if err != nil {
return nil, false, false, err
}
// Create temporary storage Access Key
2020-07-08 14:58:56 -04:00
respWithKey, err := client.SDK().IAM().AWSCompatibility().AccessKey().Create(ctx, &awscompatibility.CreateAccessKeyRequest{
ServiceAccountId: p.config.ServiceAccountID,
Description: "this temporary key is for upload image to storage; created by Packer",
2020-07-08 14:58:56 -04:00
})
if err != nil {
return nil, false, false, err
}
storageClient, err := newYCStorageClient("", respWithKey.GetAccessKey().GetKeyId(), respWithKey.GetSecret())
if err != nil {
return nil, false, false, fmt.Errorf("error create object storage client: %s", err)
}
2020-07-08 14:58:56 -04:00
switch artifact.BuilderId() {
case compress.BuilderId, artifice.BuilderId, file.BuilderId:
// Artifact as a file, need to be uploaded to storage before create Compute Image
fileSource = true
// As `bucket` option validate input here
if p.config.Bucket == "" {
return nil, false, false, fmt.Errorf("To upload artfact you need to specify `bucket` value")
}
2020-07-21 18:01:30 -04:00
imageSrc, err = uploadToBucket(storageClient, ui, artifact, p.config.Bucket, p.config.ObjectName)
if err != nil {
return nil, false, false, err
}
2020-07-21 18:01:30 -04:00
case yandexexport.BuilderId:
// Artifact already in storage, just get URL
2020-07-21 18:01:30 -04:00
imageSrc, err = presignUrl(storageClient, ui, artifact.Id())
if err != nil {
return nil, false, false, err
}
case yandex.BuilderID:
2020-07-21 18:01:30 -04:00
// Artifact is plain Yandex Compute Image, just create new one based on provided
imageSrc = &imageSource{
imageID: artifact.Id(),
}
case BuilderId:
// Artifact from prev yandex-import PP, reuse URL or Cloud Image ID
imageSrc, err = chooseSource(artifact)
if err != nil {
return nil, false, false, err
}
2020-07-08 14:58:56 -04:00
default:
err := fmt.Errorf(
"Unknown artifact type: %s\nCan only import from Yandex-Export, Yandex-Import, Compress, Artifice and File post-processor artifacts.",
2020-07-08 14:58:56 -04:00
artifact.BuilderId())
return nil, false, false, err
}
2020-07-21 18:01:30 -04:00
ycImage, err := createYCImage(ctx, client, ui, p.config.FolderID, imageSrc, p.config.ImageName, p.config.ImageDescription, p.config.ImageFamily, p.config.ImageLabels)
2020-07-08 14:58:56 -04:00
if err != nil {
return nil, false, false, err
}
if fileSource && !p.config.SkipClean {
2020-07-21 18:01:30 -04:00
err = deleteFromBucket(storageClient, ui, imageSrc)
2020-07-08 14:58:56 -04:00
if err != nil {
return nil, false, false, err
}
}
// Delete temporary storage Access Key
2020-07-08 14:58:56 -04:00
_, err = client.SDK().IAM().AWSCompatibility().AccessKey().Delete(ctx, &awscompatibility.DeleteAccessKeyRequest{
AccessKeyId: respWithKey.GetAccessKey().GetId(),
})
if err != nil {
return nil, false, false, fmt.Errorf("error delete static access key: %s", err)
}
return &Artifact{
2020-07-21 18:01:30 -04:00
imageID: ycImage.GetId(),
StateData: map[string]interface{}{
"source_type": imageSrc.GetSourceType(),
"source_id": imageSrc.GetSourceID(),
},
}, false, false, nil
2020-07-08 14:58:56 -04:00
}
2020-07-21 18:01:30 -04:00
func chooseSource(a packer.Artifact) (cloudImageSource, error) {
st := a.State("source_type").(string)
if st == "" {
return nil, fmt.Errorf("could not determine source type of yandex-import artifact: %v", a)
2020-07-08 14:58:56 -04:00
}
2020-07-21 18:01:30 -04:00
switch st {
case sourceType_IMAGE:
return &imageSource{
imageID: a.State("source_id").(string),
}, nil
2020-07-08 14:58:56 -04:00
2020-07-21 18:01:30 -04:00
case sourceType_OBJECT:
return &objectSource{
url: a.State("source_id").(string),
}, nil
2020-07-08 14:58:56 -04:00
}
2020-07-21 18:01:30 -04:00
return nil, fmt.Errorf("unknow source type of yandex-import artifact: %s", st)
2020-07-08 14:58:56 -04:00
}