Skip to content

Commit

Permalink
feat: add HCP packer support (#122)
Browse files Browse the repository at this point in the history
This pull request adds support for the HCP Packer registry by including
the necessary metadata in the image state.

For more details, see:
https://developer.hashicorp.com/packer/docs/plugins/creation/hcp-support

Continuation of #91 after accidentally closing the PR.

---------

Co-authored-by: Roman Ryzhyi <[email protected]>
Co-authored-by: Julian Tölle <[email protected]>
  • Loading branch information
3 people authored Jun 4, 2024
1 parent 9ce7de1 commit 85435ef
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 13 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ jobs:
with:
version: 1.11.0 # renovate: datasource=github-releases depName=hashicorp/packer extractVersion=v(?<version>.+)

- run: packer init .
- run: packer validate .
- run: packer build .
- run: packer init build.pkr.hcl
- run: packer validate build.pkr.hcl
- run: packer build build.pkr.hcl
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ linters:
- unparam
- unused
- whitespace

issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
32 changes: 32 additions & 0 deletions builder/hcloud/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"log"
"strconv"

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

"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

Expand Down Expand Up @@ -44,9 +46,39 @@ func (a *Artifact) String() string {
}

func (a *Artifact) State(name string) interface{} {
if name == registryimage.ArtifactStateURI {
return a.stateHCPPackerRegistryMetadata()
}
return a.StateData[name]
}

func (a *Artifact) stateHCPPackerRegistryMetadata() interface{} {
labels := make(map[string]string)

// Those labels contains the value the user specified in their template
sourceImage, ok := a.StateData["source_image"].(string)
if ok {
labels["source_image"] = sourceImage
}
serverType, ok := a.StateData["server_type"].(string)
if ok {
labels["server_type"] = serverType
}

img := &registryimage.Image{
ImageID: a.Id(),
ProviderName: "hetznercloud", // Use explicit name over the builder ID
Labels: labels,
}

sourceImageID, ok := a.StateData["source_image_id"].(int64)
if ok {
img.SourceImageID = strconv.FormatInt(sourceImageID, 10)
}

return img
}

func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
_, err := a.hcloudClient.Image.Delete(context.TODO(), &hcloud.Image{ID: a.snapshotId})
Expand Down
34 changes: 34 additions & 0 deletions builder/hcloud/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import (
"testing"

packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
registryimage "github.com/hashicorp/packer-plugin-sdk/packer/registry/image"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestArtifact_Impl(t *testing.T) {
Expand Down Expand Up @@ -58,3 +62,33 @@ func TestArtifactState_StateData(t *testing.T) {
t.Fatalf("Bad: State should be nil for nil StateData")
}
}

func TestArtifactState_hcpPackerRegistryMetadata(t *testing.T) {
artifact := &Artifact{
snapshotId: 167438588,
snapshotName: "test-image",
StateData: map[string]interface{}{
"source_image": "ubuntu-24.04",
"source_image_id": int64(161547269),
"server_type": "cpx11",
},
}

result := artifact.State(registryimage.ArtifactStateURI)
require.NotNil(t, result)

var image registryimage.Image
if err := mapstructure.Decode(result, &image); err != nil {
t.Errorf("unexpected error when trying to decode state into registryimage.Image %v", err)
}

assert.Equal(t, registryimage.Image{
ImageID: "167438588",
ProviderName: "hetznercloud",
SourceImageID: "161547269",
Labels: map[string]string{
"source_image": "ubuntu-24.04",
"server_type": "cpx11",
},
}, image)
}
7 changes: 6 additions & 1 deletion builder/hcloud/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
snapshotName: state.Get(StateSnapshotName).(string),
snapshotId: state.Get(StateSnapshotID).(int64),
hcloudClient: b.hcloudClient,
StateData: map[string]interface{}{"generated_data": state.Get(StateGeneratedData)},
StateData: map[string]interface{}{
"generated_data": state.Get(StateGeneratedData),
"source_image": b.config.Image,
"source_image_id": state.Get(StateSourceImageID),
"server_type": b.config.ServerType,
},
}

return artifact, nil
Expand Down
2 changes: 2 additions & 0 deletions builder/hcloud/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const (
StateSnapshotIDOld = "snapshot_id_old"
StateSnapshotName = "snapshot_name"
StateSSHKeyID = "ssh_key_id"

StateSourceImageID = "source_image_id"
)

func UnpackState(state multistep.StateBag) (*Config, packersdk.Ui, *hcloud.Client) {
Expand Down
12 changes: 8 additions & 4 deletions builder/hcloud/step_create_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
c, ui, client := UnpackState(state)

sshKeyId := state.Get(StateSSHKeyID).(int64)
serverType := state.Get(StateServerType).(*hcloud.ServerType)

// Create the server based on configuration
ui.Say("Creating server...")
Expand Down Expand Up @@ -50,17 +51,20 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
}

var image *hcloud.Image
var err error
if c.Image != "" {
image = &hcloud.Image{Name: c.Image}
image, _, err = client.Image.GetForArchitecture(ctx, c.Image, serverType.Architecture)
if err != nil {
return errorHandler(state, ui, "Could not find image", err)
}
} else {
serverType := state.Get(StateServerType).(*hcloud.ServerType)
var err error
image, err = getImageWithSelectors(ctx, client, c, serverType)
if err != nil {
return errorHandler(state, ui, "Could not find image", err)
}
ui.Message(fmt.Sprintf("Using image %s with ID %d", image.Description, image.ID))
}
ui.Message(fmt.Sprintf("Using image '%d'", image.ID))
state.Put(StateSourceImageID, image.ID)

var networks []*hcloud.Network
for _, k := range c.Networks {
Expand Down
57 changes: 53 additions & 4 deletions builder/hcloud/step_create_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/stretchr/testify/assert"

"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)

Expand All @@ -21,19 +22,25 @@ func TestStepCreateServer(t *testing.T) {
Step: &stepCreateServer{},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"POST", "/servers",
func(t *testing.T, r *http.Request, body []byte) {
payload := schema.ServerCreateRequest{}
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "dummy-server", payload.Name)
assert.Equal(t, "debian-12", payload.Image)
assert.Equal(t, int64(114690387), int64(payload.Image.(float64)))
assert.Equal(t, "nbg1", payload.Location)
assert.Equal(t, "cpx11", payload.ServerType)
assert.Nil(t, payload.Networks)
Expand Down Expand Up @@ -76,19 +83,25 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"POST", "/servers",
func(t *testing.T, r *http.Request, body []byte) {
payload := schema.ServerCreateRequest{}
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "dummy-server", payload.Name)
assert.Equal(t, "debian-12", payload.Image)
assert.Equal(t, int64(114690387), int64(payload.Image.(float64)))
assert.Equal(t, "nbg1", payload.Location)
assert.Equal(t, "cpx11", payload.ServerType)
assert.Equal(t, []int64{12}, payload.Networks)
Expand Down Expand Up @@ -132,13 +145,19 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"GET", "/primary_ips?name=permanent-packer-ipv4", nil,
200, `{
"primary_ips": [
Expand Down Expand Up @@ -168,7 +187,7 @@ func TestStepCreateServer(t *testing.T) {
payload := schema.ServerCreateRequest{}
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "dummy-server", payload.Name)
assert.Equal(t, "debian-12", payload.Image)
assert.Equal(t, int64(114690387), int64(payload.Image.(float64)))
assert.Equal(t, "nbg1", payload.Location)
assert.Equal(t, "cpx11", payload.ServerType)
assert.Nil(t, payload.Networks)
Expand Down Expand Up @@ -214,13 +233,19 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"GET", "/primary_ips?name=127.0.0.1", nil,
200, `{ "primary_ips": [] }`,
},
Expand Down Expand Up @@ -254,7 +279,7 @@ func TestStepCreateServer(t *testing.T) {
payload := schema.ServerCreateRequest{}
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "dummy-server", payload.Name)
assert.Equal(t, "debian-12", payload.Image)
assert.Equal(t, int64(114690387), int64(payload.Image.(float64)))
assert.Equal(t, "nbg1", payload.Location)
assert.Equal(t, "cpx11", payload.ServerType)
assert.Nil(t, payload.Networks)
Expand Down Expand Up @@ -299,13 +324,19 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"GET", "/primary_ips?name=127.0.0.1", nil,
200, `{ "primary_ips": [] }`,
},
Expand All @@ -329,13 +360,19 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"GET", "/primary_ips?name=127.0.0.1", nil,
200, `{ "primary_ips": [] }`,
},
Expand All @@ -359,13 +396,19 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"GET", "/primary_ips?name=127.0.0.1", nil,
200, `{ "primary_ips": [] }`,
},
Expand All @@ -389,13 +432,19 @@ func TestStepCreateServer(t *testing.T) {
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
state.Put(StateServerType, &hcloud.ServerType{ID: 9, Name: "cpx11", Architecture: "x86"})
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"GET", "/images?architecture=x86&include_deprecated=true&name=debian-12", nil,
200, `{
"images": [{ "id": 114690387, "name": "debian-12", "description": "Debian 12", "architecture": "x86" }]
}`,
},
{"GET", "/primary_ips?name=127.0.0.1", nil,
200, `{ "primary_ips": [] }`,
},
Expand Down
2 changes: 1 addition & 1 deletion builder/hcloud/step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func NewTestServer(t *testing.T, requests []Request) *httptest.Server {

return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if testing.Verbose() {
t.Logf("request %d: %s %s\n", index, r.Method, r.URL.Path)
t.Logf("request %d: %s %s\n", index, r.Method, r.RequestURI)
}

if index >= len(requests) {
Expand Down
Loading

0 comments on commit 85435ef

Please sign in to comment.