Skip to content
Merged
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
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,10 @@ require (
github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493
github.com/imdario/mergo v0.3.12 // indirect
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869
github.com/kr/fs v0.1.0 // indirect
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect
github.com/masterzen/winrm v0.0.0-20210623064412-3b76017826b0
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mitchellh/cli v1.1.2
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
Expand Down Expand Up @@ -99,6 +96,9 @@ require (
github.com/hashicorp/vault/sdk v0.2.1 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,6 @@ github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw=
github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.1.2 h1:Th2TIvG1+6ma3e/0/bopBKohOTY7s4dA8V2q4EUcBJ0=
github.com/mitchellh/copystructure v1.1.2/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff h1:bFJ74ac7ZK/jyislqiWdzrnENesFt43sNEBRh1xk/+g=
github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff/go.mod h1:g7SZj7ABpStq3tM4zqHiVEG5un/DZ1+qJJKO7qx1EvU=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
Expand All @@ -457,8 +455,6 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
Expand Down
70 changes: 70 additions & 0 deletions packer/registry/image/fromartifact_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package image_test

import (
"fmt"

"github.com/hashicorp/packer-plugin-sdk/packer/registry/image"
)

type simpleArtifact struct {
image_id string
}

func (a *simpleArtifact) BuilderId() string {
return "example.happycloud"
}

func (a *simpleArtifact) Files() []string {
return nil
}

func (a *simpleArtifact) Id() string {
return a.image_id
}

func (a *simpleArtifact) String() string {
return fmt.Sprintf("Imported image URL: %s", a.Id())
}

func (a *simpleArtifact) State(name string) interface{} {
return nil
}

func (a *simpleArtifact) Destroy() error {
return nil
}

func ExampleFromArtifact() {

a := &simpleArtifact{
image_id: "service-id-123",
}

hcimage, _ := image.FromArtifact(a)
fmt.Printf("%#v", *hcimage)
// Output:
// image.Image{ImageID:"service-id-123", ProviderName:"example.happycloud", ProviderRegion:"", Labels:map[string]string{}}
}

func ExampleWithProvider() {
a := &simpleArtifact{
image_id: "service-id-123",
}

// This example also includes an override for the ProviderRegion to illustrate how ArtifactOverrideFunc(s) can be chained.
hcimage, _ := image.FromArtifact(a, image.WithProvider("happycloud"), image.WithRegion("west"))
fmt.Printf("%#v", *hcimage)
// Output:
// image.Image{ImageID:"service-id-123", ProviderName:"happycloud", ProviderRegion:"west", Labels:map[string]string{}}
}

func ExampleSetLabels() {
a := &simpleArtifact{
image_id: "service-id-123",
}

hcimage, _ := image.FromArtifact(a, image.SetLabels(map[string]interface{}{"kernel": "4.0", "python": "3.5"}))
fmt.Printf("%v", hcimage.Labels)
// Unordered output:
// map[kernel:4.0 python:3.5]
}
74 changes: 74 additions & 0 deletions packer/registry/image/frommappeddata_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package image_test

import (
"errors"
"fmt"
"sort"
"strings"

"github.com/hashicorp/packer-plugin-sdk/packer/registry/image"
)

type artifact struct {
images map[string]string
}

func (a *artifact) BuilderId() string {
return "example.happycloud"
}

func (a *artifact) Files() []string {
return nil
}

func (a *artifact) Id() string {
parts := make([]string, 0, len(a.images))
for loc, id := range a.images {
parts = append(parts, loc+":"+id)
}
sort.Strings(parts)
return strings.Join(parts, ",")
}

func (a *artifact) String() string {
return a.Id()
}

func (a *artifact) State(name string) interface{} {
return nil
}

func (a *artifact) Destroy() error {
return nil
}

func ExampleFromMappedData() {
a := &artifact{
images: map[string]string{
"west": "happycloud-1",
"east": "happycloud-2",
},
}

f := func(key, value interface{}) (*image.Image, error) {
v, ok := value.(string)
if !ok {
return nil, errors.New("for happycloud maps value should always be string")
}
k, ok := key.(string)
if !ok {
return nil, errors.New("for happycloud maps key should always be string")
}

img := image.Image{ProviderName: "happycloud", ProviderRegion: k, ImageID: v}
return &img, nil
}

hcimages, _ := image.FromMappedData(a.images, f)
for _, hcimage := range hcimages {
fmt.Printf("%#v\n", *hcimage)
}
// Unordered output:
// image.Image{ImageID:"happycloud-1", ProviderName:"happycloud", ProviderRegion:"west", Labels:map[string]string(nil)}
// image.Image{ImageID:"happycloud-2", ProviderName:"happycloud", ProviderRegion:"east", Labels:map[string]string(nil)}
}
168 changes: 168 additions & 0 deletions packer/registry/image/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/* Package image allows for the management of image metadata that can be stored in a HCP Packer registry.
*/
package image

import (
"errors"
"fmt"
"reflect"

"github.com/hashicorp/packer-plugin-sdk/packer"
)

// ArtifactStateURI represents the key used by Packer when querying a packersdk.Artifact
// for Image metadata that a particular component would like to have stored on the HCP Packer Registry.
const ArtifactStateURI = "par.artifact.metadata"

// ArtifactOverrideFunc represents a transformation func that can be applied to a
// non-nil *Image. See WithID, WithRegion functions for examples.
type ArtifactOverrideFunc func(*Image) error

// Image represents the metadata for some Artifact in the HCP Packer Registry.
type Image struct {
// ImageID is a unique reference identifier stored on the HCP Packer registry
// that can be used to get back the built artifact of a builder or post-processor.
ImageID string
// ProviderName represents the name of the top level cloud or service where the built artifact resides.
// For example "aws, azure, docker, gcp, and vsphere".
ProviderName string
// ProviderRegion represents the location of the built artifact.
// For cloud providers region usually maps to a cloud region or zone, but for things like the file builder,
// S3 bucket or vsphere cluster region can represent a path on the upstream datastore, or cluster.
ProviderRegion string
// Labels represents additional details about an image that a builder or post-processor may with to provide for a given build.
// Any additional metadata will be made available as build labels within a HCP Packer registry iteration.
Labels map[string]string
}

// Validate checks that the Image i contains a non-empty ImageID and ProviderName.
func (i *Image) Validate() error {
if i.ImageID == "" {
return errors.New("error registry image does not contain a valid ImageId")
}

if i.ProviderName == "" {
return errors.New("error registry image does not contain a valid ProviderName")
}

return nil
}

// String returns string representation of Image
func (i *Image) String() string {
return fmt.Sprintf("provider:%s, image:%s, region:%s", i.ProviderName, i.ImageID, i.ProviderRegion)
}

// FromMappedData calls f sequentially for each key and value present in mappedData to create a []*Image
// as the final return value. If there is an error in calling f, FromMappedData will stop processing mapped items
// and result in a nil slice, with the said error.
//
// FromMappedData will make its best attempt to convert the input map into map[interface{}]interface{} before
// calling f(k,v). The func f is responsible for type asserting the expected type for the key and value before
// trying to create an Image from it.
func FromMappedData(mappedData interface{}, f func(key, value interface{}) (*Image, error)) ([]*Image, error) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative to this implementation which would allow for a map of any type. I'm considering just supporting a mappedData input of map[string]string, as this seems to be the std. map being used across most plugins that return multi-region artifacts.

It also removes the need for the consumer to do type assertions in the iteration function, which seems a bit complex.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason you didn't go to map[string]string from the beginning?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flexibility really. I thought maybe it would be useful to others if I didn't define the concrete type.


mapValue := reflect.ValueOf(mappedData)
if mapValue.Kind() != reflect.Map {
return nil, errors.New("error the incoming mappedData does not appear to be a map; found type to be" + mapValue.Kind().String())
}

keys := mapValue.MapKeys()
var images []*Image
for _, k := range keys {
v := mapValue.MapIndex(k)
i, err := f(k.Interface(), v.Interface())
if err != nil {
return nil, err
}
images = append(images, i)
}
return images, nil
}

// FromArtifact returns an *Image that can be used by Packer core for publishing to the HCP Packer Registry.
// By default FromArtifact will use the a.BuilderID() as the ProviderName, and the a.Id() as the ImageID that
// should be tracked within the HCP Packer Registry. No Region is selected by default as region varies per builder.
// The use of one or more ArtifactOverrideFunc can be used to override any of the defaults used.
func FromArtifact(a packer.Artifact, opts ...ArtifactOverrideFunc) (*Image, error) {
if a == nil {
return nil, errors.New("unable to create Image from nil artifact")
}

img := Image{
ProviderName: a.BuilderId(),
ImageID: a.Id(),
Labels: make(map[string]string),
}

for _, opt := range opts {
err := opt(&img)
if err != nil {
return nil, err
}
}

return &img, nil
}

// WithProvider takes a name, and returns a ArtifactOverrideFunc that can be
// used to override the ProviderName for an existing Image.
func WithProvider(name string) func(*Image) error {
return func(img *Image) error {
if img == nil {
return errors.New("no go on empty image")
}

img.ProviderName = name
return nil
}
}

// WithID takes a id, and returns a ArtifactOverrideFunc that can be
// used to override the ImageId for an existing Image.
func WithID(id string) func(*Image) error {
return func(img *Image) error {
if img == nil {
return errors.New("no go on empty image")
}

img.ImageID = id
return nil
}
}

// WithRegion takes a region, and returns a ArtifactOverrideFunc that can be
// used to override the ProviderRegion for an existing Image.
func WithRegion(region string) func(*Image) error {
return func(img *Image) error {
if img == nil {
return errors.New("no go on empty image")
}

img.ProviderRegion = region
return nil
}
}

// SetLabels takes metadata, and returns a ArtifactOverrideFunc that can be
// used to set metadata for an existing Image. The incoming metadata `md`
// will be filtered only for keys whose values are of type string.
// If you wish to override this behavior you may create your own ArtifactOverrideFunc
// for manipulating and setting Image metadata.
func SetLabels(md map[string]interface{}) func(*Image) error {
return func(img *Image) error {
if img.Labels == nil {
img.Labels = make(map[string]string)
}

for k, v := range md {
v, ok := v.(string)
if !ok {
continue
}
img.Labels[k] = v
}

return nil
}
}
Loading