Add Exoscale Import post-processor

This change adds a new `exoscale-import` post-processor allowing users
to create Private Templates on Exoscale.
This commit is contained in:
Marc Falzon 2019-06-18 19:07:32 +02:00
parent 049811d329
commit 00c2df24df
7 changed files with 380 additions and 0 deletions

View File

@ -63,6 +63,7 @@ import (
dockerpushpostprocessor "github.com/hashicorp/packer/post-processor/docker-push"
dockersavepostprocessor "github.com/hashicorp/packer/post-processor/docker-save"
dockertagpostprocessor "github.com/hashicorp/packer/post-processor/docker-tag"
exoscaleimportpostprocessor "github.com/hashicorp/packer/post-processor/exoscale-import"
googlecomputeexportpostprocessor "github.com/hashicorp/packer/post-processor/googlecompute-export"
googlecomputeimportpostprocessor "github.com/hashicorp/packer/post-processor/googlecompute-import"
manifestpostprocessor "github.com/hashicorp/packer/post-processor/manifest"
@ -168,6 +169,7 @@ var PostProcessors = map[string]packer.PostProcessor{
"docker-push": new(dockerpushpostprocessor.PostProcessor),
"docker-save": new(dockersavepostprocessor.PostProcessor),
"docker-tag": new(dockertagpostprocessor.PostProcessor),
"exoscale-import": new(exoscaleimportpostprocessor.PostProcessor),
"googlecompute-export": new(googlecomputeexportpostprocessor.PostProcessor),
"googlecompute-import": new(googlecomputeimportpostprocessor.PostProcessor),
"manifest": new(manifestpostprocessor.PostProcessor),

1
go.mod
View File

@ -31,6 +31,7 @@ require (
github.com/docker/docker v0.0.0-20180422163414-57142e89befe // indirect
github.com/dylanmei/iso8601 v0.1.0 // indirect
github.com/dylanmei/winrmtest v0.0.0-20170819153634-c2fbb09e6c08
github.com/exoscale/egoscale v0.18.1
github.com/go-ini/ini v1.25.4
github.com/gofrs/flock v0.7.1
github.com/google/go-cmp v0.2.0

4
go.sum
View File

@ -106,6 +106,8 @@ github.com/dylanmei/winrmtest v0.0.0-20170819153634-c2fbb09e6c08/go.mod h1:VBVDF
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/exoscale/egoscale v0.18.1 h1:1FNZVk8jHUx0AvWhOZxLEDNlacTU0chMXUUNkm9EZaI=
github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@ -121,6 +123,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc=
github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=

View File

@ -0,0 +1,31 @@
package exoscaleimport
const BuilderId = "packer.post-processor.exoscale-import"
type Artifact struct {
id string
}
func (a *Artifact) BuilderId() string {
return BuilderId
}
func (a *Artifact) Id() string {
return a.id
}
func (a *Artifact) Files() []string {
return nil
}
func (a *Artifact) String() string {
return a.id
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
return nil
}

View File

@ -0,0 +1,250 @@
package exoscaleimport
import (
"context"
"crypto/md5"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/exoscale/egoscale"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/version"
)
var (
defaultTemplateZone = "ch-gva-2"
defaultAPIEndpoint = "https://api.exoscale.com/compute"
defaultSOSEndpoint = "https://sos-" + defaultTemplateZone + ".exo.io"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
SkipClean bool `mapstructure:"skip_clean"`
APIEndpoint string `mapstructure:"api_endpoint"`
SOSEndpoint string `mapstructure:"sos_endpoint"`
APIKey string `mapstructure:"api_key"`
APISecret string `mapstructure:"api_secret"`
ImageBucket string `mapstructure:"image_bucket"`
TemplateZone string `mapstructure:"template_zone"`
TemplateName string `mapstructure:"template_name"`
TemplateDescription string `mapstructure:"template_description"`
TemplateUsername string `mapstructure:"template_username"`
TemplateDisablePassword bool `mapstructure:"template_disable_password"`
TemplateDisableSSHKey bool `mapstructure:"template_disable_sshkey"`
}
func init() {
egoscale.UserAgent = "Packer-Exoscale/" + version.FormattedVersion() + " " + egoscale.UserAgent
}
type PostProcessor struct {
config Config
}
func (p *PostProcessor) Configure(raws ...interface{}) error {
p.config.TemplateZone = defaultTemplateZone
p.config.APIEndpoint = defaultAPIEndpoint
p.config.SOSEndpoint = defaultSOSEndpoint
if err := config.Decode(&p.config, nil, raws...); err != nil {
return err
}
if p.config.APIKey == "" {
p.config.APIKey = os.Getenv("EXOSCALE_API_KEY")
}
if p.config.APISecret == "" {
p.config.APISecret = os.Getenv("EXOSCALE_API_SECRET")
}
requiredArgs := map[string]*string{
"api_key": &p.config.APIKey,
"api_secret": &p.config.APISecret,
"api_endpoint": &p.config.APIEndpoint,
"sos_endpoint": &p.config.SOSEndpoint,
"image_bucket": &p.config.ImageBucket,
"template_zone": &p.config.TemplateZone,
"template_name": &p.config.TemplateName,
}
errs := new(packer.MultiError)
for k, v := range requiredArgs {
if *v == "" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("%s must be set", k))
}
}
if len(errs.Errors) > 0 {
return errs
}
packer.LogSecretFilter.Set(p.config.APIKey, p.config.APISecret)
return nil
}
func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, a packer.Artifact) (packer.Artifact, bool, bool, error) {
ui.Message("Uploading template image")
url, md5sum, err := p.uploadImage(ctx, ui, a)
if err != nil {
return nil, false, false, fmt.Errorf("unable to upload image: %s", err)
}
ui.Message("Registering template")
id, err := p.registerTemplate(ctx, ui, url, md5sum)
if err != nil {
return nil, false, false, fmt.Errorf("unable to register template: %s", err)
}
if !p.config.SkipClean {
ui.Message("Deleting uploaded template image")
if err = p.deleteImage(ctx, ui, a); err != nil {
return nil, false, false, fmt.Errorf("unable to delete uploaded template image: %s", err)
}
}
return &Artifact{id}, false, false, nil
}
func (p *PostProcessor) uploadImage(ctx context.Context, ui packer.Ui, a packer.Artifact) (string, string, error) {
var (
imageFile = a.Files()[0]
bucketFile = filepath.Base(imageFile)
)
f, err := os.Open(imageFile)
if err != nil {
return "", "", err
}
defer f.Close()
fileInfo, err := f.Stat()
if err != nil {
return "", "", err
}
// For tracking image file upload progress
pf := ui.TrackProgress(imageFile, 0, fileInfo.Size(), f)
defer pf.Close()
hash := md5.New()
if _, err := io.Copy(hash, f); err != nil {
return "", "", fmt.Errorf("image checksumming failed: %s", err)
}
if _, err := f.Seek(0, 0); err != nil {
return "", "", err
}
sess := session.Must(session.NewSessionWithOptions(session.Options{Config: aws.Config{
Region: aws.String(p.config.TemplateZone),
Endpoint: aws.String(p.config.SOSEndpoint),
Credentials: credentials.NewStaticCredentials(p.config.APIKey, p.config.APISecret, "")}}))
uploader := s3manager.NewUploader(sess)
output, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Body: pf,
Bucket: aws.String(p.config.ImageBucket),
Key: aws.String(bucketFile),
ContentMD5: aws.String(base64.StdEncoding.EncodeToString(hash.Sum(nil))),
ACL: aws.String("public-read"),
})
if err != nil {
return "", "", err
}
return output.Location, fmt.Sprintf("%x", hash.Sum(nil)), nil
}
func (p *PostProcessor) deleteImage(ctx context.Context, ui packer.Ui, a packer.Artifact) error {
var (
imageFile = a.Files()[0]
bucketFile = filepath.Base(imageFile)
)
sess := session.Must(session.NewSessionWithOptions(session.Options{Config: aws.Config{
Region: aws.String(p.config.TemplateZone),
Endpoint: aws.String(p.config.SOSEndpoint),
Credentials: credentials.NewStaticCredentials(p.config.APIKey, p.config.APISecret, "")}}))
svc := s3.New(sess)
if _, err := svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(p.config.ImageBucket),
Key: aws.String(bucketFile),
}); err != nil {
return err
}
return nil
}
func (p *PostProcessor) registerTemplate(ctx context.Context, ui packer.Ui, url, md5sum string) (string, error) {
var (
passwordEnabled = !p.config.TemplateDisablePassword
sshkeyEnabled = !p.config.TemplateDisableSSHKey
regErr error
)
exo := egoscale.NewClient(p.config.APIEndpoint, p.config.APIKey, p.config.APISecret)
exo.RetryStrategy = egoscale.FibonacciRetryStrategy
zone := egoscale.Zone{Name: p.config.TemplateZone}
if resp, err := exo.GetWithContext(ctx, &zone); err != nil {
return "", fmt.Errorf("template zone lookup failed: %s", err)
} else {
zone.ID = resp.(*egoscale.Zone).ID
}
req := egoscale.RegisterCustomTemplate{
URL: url,
ZoneID: zone.ID,
Name: p.config.TemplateName,
Displaytext: p.config.TemplateDescription,
PasswordEnabled: &passwordEnabled,
SSHKeyEnabled: &sshkeyEnabled,
Details: map[string]string{"username": p.config.TemplateUsername},
Checksum: md5sum,
}
res := make([]egoscale.Template, 0)
exo.AsyncRequestWithContext(ctx, req, func(jobRes *egoscale.AsyncJobResult, err error) bool {
if err != nil {
regErr = fmt.Errorf("request failed: %s", err)
return false
} else if jobRes.JobStatus == egoscale.Pending {
// Job is not completed yet
ui.Message("template registration in progress")
return true
}
if err := jobRes.Result(&res); err != nil {
regErr = err
return false
}
if len(res) != 1 {
regErr = fmt.Errorf("unexpected response from API (expected 1 item, got %d)", len(res))
return false
}
return false
})
if regErr != nil {
return "", regErr
}
return res[0].ID.String(), nil
}

View File

@ -0,0 +1,89 @@
---
description: |
The Packer Exoscale Import post-processor takes an image artifact
from various builders and imports it to Exoscale.
layout: docs
page_title: 'Exoscale Import - Post-Processors'
sidebar_current: 'docs-post-processors-exoscale-import'
---
# Exoscale Import Post-Processor
Type: `exoscale-import`
The Packer Exoscale Import post-processor takes an image artifact from
various builders and imports it to Exoscale.
## How Does it Work?
The import process operates uploading a temporary copy of the image to
Exoscale's [Object Storage](https://www.exoscale.com/object-storage/) (SOS)
and then importing it as a Private Template via the Exoscale API. The
temporary copy in SOS can be discarded after the import is complete.
For more information about Exoscale Private Templates, see the
[documentation](https://community.exoscale.com/documentation/compute/private-templates/).
## Configuration
There are some configuration options available for the post-processor.
Required:
- `api_key` (string) - The API key used to communicate with Exoscale
services. This may also be set using the `EXOSCALE_API_KEY` environmental
variable.
- `api_secret` (string) - The API secret used to communicate with Exoscale
services. This may also be set using the `EXOSCALE_API_SECRET`
environmental variable.
- `image_bucket` (string) - The name of the bucket in which to upload the
template image to SOS. The bucket must exist when the post-processor is
run.
- `template_name` (string) - The name to be used for registering the template.
Optional:
- `api_endpoint` (string) - The API endpoint used to communicate with the
Exoscale API. Defaults to `https://api.exoscale.com/compute`.
- `sos_endpoint` (string) - The endpoint used to communicate with SOS.
Defaults to `https://sos-ch-gva-2.exo.io`.
- `template_zone` (string) - The Exoscale [zone](https://www.exoscale.com/datacenters/)
in which to register the template. Defaults to `ch-gva-2`.
- `template_description` (string) - An optional text description for the
registered template.
- `template_username` (string) - An optional username to be used to log into
Compute instances using this template.
- `template_disable_password` (boolean) - Whether the registered template
should disable Compute instance password reset. Defaults to `false`.
- `template_disable_sshkey` (boolean) - Whether the registered template
should disable SSH key installation during Compute instance creation.
Defaults to `false`.
- `skip_clean` (boolean) - Whether we should skip removing the image file
uploaded to SOS after the import process has completed. "true" means that
we should leave it in the bucket, "false" means deleting it.
Defaults to `false`.
## Basic Example
Here is a basic example:
``` json
{
"type": "exoscale-import",
"api_key": "{{user `exoscale_api_key`}}",
"api_secret": "{{user `exoscale_api_secret`}}",
"image_bucket": "my-templates",
"template_name": "myapp",
"template_username": "admin"
}
```

View File

@ -302,6 +302,9 @@
<li<%= sidebar_current("docs-post-processors-docker-tag") %>>
<a href="/docs/post-processors/docker-tag.html">Docker Tag</a>
</li>
<li<%= sidebar_current("docs-post-processors-exoscale-import") %>>
<a href="/docs/post-processors/exoscale-import.html">Exoscale Import</a>
</li>
<li<%= sidebar_current("docs-post-processors-googlecompute-export") %>>
<a href="/docs/post-processors/googlecompute-export.html">Google Compute Export</a>
</li>