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:
parent
049811d329
commit
00c2df24df
|
@ -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
1
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
```
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue