Skip to content

Commit

Permalink
Add networks in config to allow attaching server to private net
Browse files Browse the repository at this point in the history
  • Loading branch information
magec authored and lbajolet-hashicorp committed Sep 25, 2023
1 parent 4156b33 commit 82e6bc1
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .web-docs/components/builder/hcloud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ builder.
be upgraded to, without changing the disk size. Improves building performance.
The resulting snapshot is compatible with smaller server types and disk sizes.

- `networks` (array of integers) - List of Network IDs which should be
attached to the server private network interface at creation time.

## Basic Example

Here is a basic example. It is completely valid as soon as you enter your own
Expand Down
1 change: 1 addition & 0 deletions builder/hcloud/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Config struct {
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
SSHKeys []string `mapstructure:"ssh_keys"`
Networks []int64 `mapstructure:"networks"`

RescueMode string `mapstructure:"rescue"`

Expand Down
2 changes: 2 additions & 0 deletions builder/hcloud/config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions builder/hcloud/step_create_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,27 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
ui.Message(fmt.Sprintf("Using image %s with ID %d", image.Description, image.ID))
}

var networks []*hcloud.Network
for _, k := range c.Networks {
networks = append(networks, &hcloud.Network{ID: k})
}

serverCreateOpts := hcloud.ServerCreateOpts{
Name: c.ServerName,
ServerType: &hcloud.ServerType{Name: c.ServerType},
Image: image,
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: c.Location},
UserData: userData,
Networks: networks,
}

if c.UpgradeServerType != "" {
serverCreateOpts.StartAfterCreate = hcloud.Bool(false)
}

serverCreateResult, _, err := client.Server.Create(ctx, serverCreateOpts)

if err != nil {
err := fmt.Errorf("Error creating server: %s", err)
state.Put("error", err)
Expand Down
203 changes: 203 additions & 0 deletions builder/hcloud/step_create_server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package hcloud

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)

type Checker func(requestBody string, path string) error

func TestStepCreateServer(t *testing.T) {
const snapName = "dummy-snap"
const imageName = "dummy-image"
const name = "dummy-name"
const location = "nbg1"
const serverType = "cpx11"
networks := []int64{1}

testCases := []struct {
name string
config Config
check Checker
wantAction multistep.StepAction
}{
{
name: "happy path",
wantAction: multistep.ActionContinue,
check: func(r string, path string) error {
if path == "/servers" {
payload := schema.ServerCreateRequest{}
err := json.Unmarshal([]byte(r), &payload)
if err != nil {
t.Errorf("server request not a json: got: (%s)", err)
}

if payload.Name != name {
t.Errorf("Incorrect name in request, expected '%s' found '%s'", name, payload.Name)
}

if payload.Image != imageName {
t.Errorf("Incorrect image in request, expected '%s' found '%s'", imageName, payload.Image)
}

if payload.Location != location {
t.Errorf("Incorrect location in request, expected '%s' found '%s'", location, payload.Location)
}

if payload.ServerType != serverType {
t.Errorf("Incorrect serverType in request, expected '%s' found '%s'", serverType, payload.ServerType)
}
if payload.Networks != nil {
t.Error("Networks should not be specified")
}
}
return nil
},
},
{
name: "with netowork",
wantAction: multistep.ActionContinue,
config: Config{
Networks: networks,
},
check: func(r string, path string) error {
if path == "/servers" {
payload := schema.ServerCreateRequest{}
err := json.Unmarshal([]byte(r), &payload)
if err != nil {
t.Errorf("server request not a json: (%s)", err)
}
if payload.Networks[0] != networks[0] {
t.Errorf("network not set")
}
}
return nil
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
errors := make(chan error, 1)
state, teardown := setupStepCreateServer(errors, tc.check)
defer teardown()

step := &stepCreateServer{}

baseConfig := Config{
ServerName: name,
Image: imageName,
SnapshotName: snapName,
ServerType: serverType,
Location: location,
SSHKeys: []string{"1"},
}

config := baseConfig
config.Networks = tc.config.Networks

if testing.Verbose() {
state.Put("ui", packersdk.TestUi(t))
} else {
// do not output to stdout or console
state.Put("ui", &packersdk.MockUi{})
}
state.Put("config", &config)
state.Put("ssh_key_id", int64(1))

if action := step.Run(context.Background(), state); action != tc.wantAction {
t.Errorf("step.Run: want: %v; got: %v", tc.wantAction, action)
}

select {
case err := <-errors:
t.Errorf("server: got: %s", err)
default:
}
})
}
}

// Configure a httptest server to reply to the requests done by stepCreateSnapshot.
// React with the appropriate failCause.
// Report errors on the errors channel (cannot use testing.T, it runs on a different goroutine).
// Return a tuple (state, teardown) where:
// - state (containing the client) is ready to be passed to the step.Run() method.
// - teardown is a function meant to be deferred from the test.
func setupStepCreateServer(
errors chan<- error,
checker Checker,
) (*multistep.BasicStateBag, func()) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

buf, err := io.ReadAll(r.Body)
if err != nil {
errors <- fmt.Errorf("fake server: reading request: %s", err)
return
}
reqDump := fmt.Sprintf("fake server: request:\n %s %s\n body: %s",
r.Method, r.URL.Path, string(buf))
if testing.Verbose() {
fmt.Println(reqDump)
}

enc := json.NewEncoder(w)
var response interface{}
action := schema.Action{
ID: 1,
Status: "success",
}

if r.Method == http.MethodPost && r.URL.Path == "/servers" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
response = schema.ServerCreateResponse{Action: action}
}

if r.Method == http.MethodGet && r.URL.Path == "/actions/1" {
w.Header().Set("Content-Type", "application/json")
response = schema.ActionGetResponse{Action: action}
}

if r.Method == http.MethodGet && r.URL.Path == "/ssh_keys/1" {
w.Header().Set("Content-Type", "application/json")
response = schema.SSHKeyGetResponse{
SSHKey: schema.SSHKey{ID: 1},
}
}

if err := checker(string(buf), r.URL.Path); err != nil {
errors <- fmt.Errorf("Error in checker")
}

if response != nil {
if err := enc.Encode(response); err != nil {
errors <- fmt.Errorf("fake server: encoding reply: %s", err)
}
return
}

// no match: report error
w.WriteHeader(http.StatusBadRequest)
errors <- fmt.Errorf(reqDump)
}))

state := multistep.BasicStateBag{}
client := hcloud.NewClient(hcloud.WithEndpoint(ts.URL))
state.Put("hcloudClient", client)

teardown := func() {
ts.Close()
}
return &state, teardown
}
3 changes: 3 additions & 0 deletions docs/builders/hcloud.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ builder.
be upgraded to, without changing the disk size. Improves building performance.
The resulting snapshot is compatible with smaller server types and disk sizes.

- `networks` (array of integers) - List of Network IDs which should be
attached to the server private network interface at creation time.

## Basic Example

Here is a basic example. It is completely valid as soon as you enter your own
Expand Down

0 comments on commit 82e6bc1

Please sign in to comment.