diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85d223c3..f07efc32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,6 @@ jobs: with: version: 1.11.0 # renovate: datasource=github-releases depName=hashicorp/packer extractVersion=v(?.+) - - 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 diff --git a/.golangci.yml b/.golangci.yml index 2d42fb9c..2a33cb51 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,3 +40,9 @@ linters: - unparam - unused - whitespace + +issues: + exclude-rules: + - path: _test\.go + linters: + - dupl diff --git a/builder/hcloud/artifact.go b/builder/hcloud/artifact.go index 27783050..02578aef 100644 --- a/builder/hcloud/artifact.go +++ b/builder/hcloud/artifact.go @@ -9,6 +9,8 @@ import ( "log" "strconv" + registryimage "github.com/hashicorp/packer-plugin-sdk/packer/registry/image" + "github.com/hetznercloud/hcloud-go/v2/hcloud" ) @@ -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 := ®istryimage.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}) diff --git a/builder/hcloud/artifact_test.go b/builder/hcloud/artifact_test.go index 25870859..1f512367 100644 --- a/builder/hcloud/artifact_test.go +++ b/builder/hcloud/artifact_test.go @@ -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) { @@ -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) +} diff --git a/builder/hcloud/builder.go b/builder/hcloud/builder.go index a01e6b1d..149c422d 100644 --- a/builder/hcloud/builder.go +++ b/builder/hcloud/builder.go @@ -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 diff --git a/builder/hcloud/state.go b/builder/hcloud/state.go index 47aa09d5..595dfcb1 100644 --- a/builder/hcloud/state.go +++ b/builder/hcloud/state.go @@ -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) { diff --git a/builder/hcloud/step_create_server.go b/builder/hcloud/step_create_server.go index 19492241..321dc9df 100644 --- a/builder/hcloud/step_create_server.go +++ b/builder/hcloud/step_create_server.go @@ -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...") @@ -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 { diff --git a/builder/hcloud/step_create_server_test.go b/builder/hcloud/step_create_server_test.go index 481d95c7..3de87371 100644 --- a/builder/hcloud/step_create_server_test.go +++ b/builder/hcloud/step_create_server_test.go @@ -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" ) @@ -21,6 +22,7 @@ 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, @@ -28,12 +30,17 @@ func TestStepCreateServer(t *testing.T) { "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) @@ -76,6 +83,7 @@ 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, @@ -83,12 +91,17 @@ func TestStepCreateServer(t *testing.T) { "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) @@ -132,6 +145,7 @@ 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, @@ -139,6 +153,11 @@ func TestStepCreateServer(t *testing.T) { "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": [ @@ -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) @@ -214,6 +233,7 @@ 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, @@ -221,6 +241,11 @@ func TestStepCreateServer(t *testing.T) { "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": [] }`, }, @@ -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) @@ -299,6 +324,7 @@ 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, @@ -306,6 +332,11 @@ func TestStepCreateServer(t *testing.T) { "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": [] }`, }, @@ -329,6 +360,7 @@ 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, @@ -336,6 +368,11 @@ func TestStepCreateServer(t *testing.T) { "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": [] }`, }, @@ -359,6 +396,7 @@ 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, @@ -366,6 +404,11 @@ func TestStepCreateServer(t *testing.T) { "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": [] }`, }, @@ -389,6 +432,7 @@ 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, @@ -396,6 +440,11 @@ func TestStepCreateServer(t *testing.T) { "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": [] }`, }, diff --git a/builder/hcloud/step_test.go b/builder/hcloud/step_test.go index 113b4ef2..b62fcca6 100644 --- a/builder/hcloud/step_test.go +++ b/builder/hcloud/step_test.go @@ -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) { diff --git a/example/build_hcp.pkr.hcl b/example/build_hcp.pkr.hcl new file mode 100644 index 00000000..5ecb92f9 --- /dev/null +++ b/example/build_hcp.pkr.hcl @@ -0,0 +1,53 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +packer { + required_plugins { + hcloud = { + source = "github.com/hetznercloud/hcloud" + version = ">=1.1.0" + } + } +} + +variable "hcloud_token" { + type = string + sensitive = true + default = "${env("HCLOUD_TOKEN")}" +} + +source "hcloud" "example" { + token = var.hcloud_token + + location = "hel1" + image = "ubuntu-24.04" + server_type = "cpx11" + server_name = "hcloud-example" + + ssh_username = "root" + + snapshot_name = "hcloud-example" + snapshot_labels = { + app = "hcloud-example" + } +} + +build { + hcp_packer_registry { + description = "A nice test description" + bucket_name = "hcloud-hcp-test" + bucket_labels = { + "packer version" = packer.version + } + } + + sources = ["source.hcloud.example"] + + provisioner "shell" { + inline = ["cloud-init status --wait || test $? -eq 2"] + } + + provisioner "shell" { + inline = ["echo 'Hello World!' > /var/log/packer.log"] + } +}