Skip to content

Commit

Permalink
Server Create without primary IPs via public_net
Browse files Browse the repository at this point in the history
  • Loading branch information
4ND3R50N committed Jul 4, 2022
1 parent 1937ee9 commit 77d5bde
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 76 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ require (
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.14.0
github.com/hetznercloud/hcloud-go v1.35.0
github.com/hetznercloud/hcloud-go v1.35.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c h1:nqkErwUGfpZZMqj29WZ9U/wz2OpJVDuiokLhE/3Y7IQ=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hetznercloud/hcloud-go v1.35.0 h1:sduXOrWM0/sJXwBty7EQd7+RXEJh5+CsAGQmHshChFg=
github.com/hetznercloud/hcloud-go v1.35.0/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA=
github.com/hetznercloud/hcloud-go v1.35.1 h1:/9d9BCWDavHbsUee5ECUNABiiTlZEfVSIsdbdq3tXHc=
github.com/hetznercloud/hcloud-go v1.35.1/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
Expand Down
94 changes: 68 additions & 26 deletions internal/e2etests/primaryip/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hetznercloud/hcloud-go/hcloud"
Expand All @@ -17,7 +19,7 @@ import (
)

const (
testDatacenter = "fsn1-dc14"
testDatacenter = "hel1-dc2"
)

func TestPrimaryIPResource_Basic(t *testing.T) {
Expand Down Expand Up @@ -84,36 +86,50 @@ func TestPrimaryIPResource_Basic(t *testing.T) {

func TestPrimaryIPResource_with_server(t *testing.T) {
var srv hcloud.Server
var pip hcloud.PrimaryIP
var pip2 hcloud.PrimaryIP
primaryIPOneRes := &primaryip.RData{
Name: "primaryip-test",
var primaryIPv4One hcloud.PrimaryIP
var primaryIPv4Two hcloud.PrimaryIP
var primaryIPv6One hcloud.PrimaryIP
primaryIPv4OneRes := &primaryip.RData{
Name: "primaryip-test-v4-one",
Type: "ipv4",
Labels: nil,
Datacenter: testDatacenter,
AssigneeType: "server",
AutoDelete: false,
}
primaryIPOneRes.SetRName("primary_ip_test")
primaryIPv4OneRes.SetRName("primary_ip_v4_test")

primaryIPv6OneRes := &primaryip.RData{
Name: "primaryip-test-v6-one",
Type: "ipv6",
Labels: nil,
Datacenter: testDatacenter,
AssigneeType: "server",
AutoDelete: false,
}
primaryIPv6OneRes.SetRName("primary_ip_v6_test")

primaryIPTwoRes := &primaryip.RData{
Name: "primaryip-test_2",
primaryIPv4TwoRes := &primaryip.RData{
Name: "primaryip-test-v4-two",
Type: "ipv4",
Labels: nil,
Datacenter: testDatacenter,
AssigneeType: "server",
AutoDelete: false,
}
primaryIPTwoRes.SetRName("primary_ip_test_2")
primaryIPv4TwoRes.SetRName("primary_ip_v4_two_test")

testServerRes := &server.RData{
Name: "server-test",
Type: e2etests.TestServerType,
Image: e2etests.TestImage,
Datacenter: testDatacenter,
Labels: nil,
PublicNet: map[string]string{
"ipv4": primaryIPOneRes.TFID() + ".id",
PublicNet: map[string]interface{}{
"ipv4_enabled": true,
"ipv6_enabled": true,
"ipv4": primaryIPv4OneRes.TFID() + ".id",
"ipv6": primaryIPv6OneRes.TFID() + ".id",
},
}

Expand All @@ -123,8 +139,9 @@ func TestPrimaryIPResource_with_server(t *testing.T) {
Image: testServerRes.Image,
Datacenter: testServerRes.Datacenter,
Labels: testServerRes.Labels,
PublicNet: map[string]string{
"ipv4": primaryIPTwoRes.TFID() + ".id",
PublicNet: map[string]interface{}{
"ipv4": primaryIPv4TwoRes.TFID() + ".id",
"ipv6_enabled": false,
},
}
testServerUpdatedRes.SetRName(testServerRes.RName())
Expand All @@ -139,37 +156,62 @@ func TestPrimaryIPResource_with_server(t *testing.T) {
},
CheckDestroy: resource.ComposeAggregateTestCheckFunc(
testsupport.CheckResourcesDestroyed(server.ResourceType, server.ByID(t, &srv)),
testsupport.CheckResourcesDestroyed(primaryip.ResourceType, primaryip.ByID(t, &pip)),
testsupport.CheckResourcesDestroyed(primaryip.ResourceType, primaryip.ByID(t, &pip2)),
testsupport.CheckResourcesDestroyed(primaryip.ResourceType, primaryip.ByID(t, &primaryIPv4One)),
testsupport.CheckResourcesDestroyed(primaryip.ResourceType, primaryip.ByID(t, &primaryIPv4Two)),
testsupport.CheckResourcesDestroyed(primaryip.ResourceType, primaryip.ByID(t, &primaryIPv6One)),
),
Steps: []resource.TestStep{
{
// Create a new primary ip & server using the required values
// only.
Config: tmplMan.Render(t,
"testdata/r/hcloud_primary_ip", primaryIPOneRes,
"testdata/r/hcloud_primary_ip", primaryIPTwoRes,
"testdata/r/hcloud_primary_ip", primaryIPv4OneRes,
"testdata/r/hcloud_primary_ip", primaryIPv6OneRes,
"testdata/r/hcloud_primary_ip", primaryIPv4TwoRes,
"testdata/r/hcloud_server", testServerRes),

Check: resource.ComposeTestCheckFunc(
testsupport.CheckResourceExists(primaryIPOneRes.TFID(), primaryip.ByID(t, &pip)),
testsupport.CheckResourceExists(primaryIPTwoRes.TFID(), primaryip.ByID(t, &pip2)),
testsupport.CheckResourceExists(primaryIPv4OneRes.TFID(), primaryip.ByID(t, &primaryIPv4One)),
testsupport.CheckResourceExists(primaryIPv4TwoRes.TFID(), primaryip.ByID(t, &primaryIPv4Two)),
testsupport.CheckResourceExists(primaryIPv6OneRes.TFID(), primaryip.ByID(t, &primaryIPv6One)),
testsupport.CheckResourceExists(testServerRes.TFID(), server.ByID(t, &srv)),
resource.TestCheckResourceAttr(primaryIPOneRes.TFID(), "name",
fmt.Sprintf("primaryip-test--%d", tmplMan.RandInt)),
resource.TestCheckResourceAttr(primaryIPv4OneRes.TFID(), "name",
fmt.Sprintf("primaryip-test-v4-one--%d", tmplMan.RandInt)),
resource.TestCheckResourceAttr(primaryIPv6OneRes.TFID(), "name",
fmt.Sprintf("primaryip-test-v6-one--%d", tmplMan.RandInt)),
resource.TestCheckResourceAttr(testServerRes.TFID(), "name",
fmt.Sprintf("server-test--%d", tmplMan.RandInt)),
resource.TestCheckResourceAttr(primaryIPOneRes.TFID(), "type", primaryIPOneRes.Type),
resource.TestCheckResourceAttr(primaryIPv4OneRes.TFID(), "type", primaryIPv4OneRes.Type),
resource.TestCheckResourceAttr(testServerRes.TFID(), "server_type", testServerRes.Type),
resource.TestCheckResourceAttr(primaryIPOneRes.TFID(), "assignee_id", strconv.Itoa(pip.ID)),
resource.TestCheckResourceAttr(primaryIPv4OneRes.TFID(), "assignee_id", strconv.Itoa(primaryIPv4One.ID)),
),
},
{
Config: tmplMan.Render(t, "testdata/r/hcloud_primary_ip", primaryIPOneRes,
"testdata/r/hcloud_primary_ip", primaryIPTwoRes,
Config: tmplMan.Render(t, "testdata/r/hcloud_primary_ip", primaryIPv4OneRes,
"testdata/r/hcloud_primary_ip", primaryIPv6OneRes,
"testdata/r/hcloud_primary_ip", primaryIPv4TwoRes,
"testdata/r/hcloud_server", testServerUpdatedRes),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(primaryIPTwoRes.TFID(), "assignee_id", strconv.Itoa(pip2.ID))),
// assign current hcloud primary ips + new server to local variables + check its existence
testsupport.CheckResourceExists(primaryIPv4OneRes.TFID(), primaryip.ByID(t, &primaryIPv4One)),
testsupport.CheckResourceExists(primaryIPv4TwoRes.TFID(), primaryip.ByID(t, &primaryIPv4Two)),
testsupport.CheckResourceExists(primaryIPv6OneRes.TFID(), primaryip.ByID(t, &primaryIPv6One)),
testsupport.CheckResourceExists(testServerUpdatedRes.TFID(), server.ByID(t, &srv)),
func(_ *terraform.State) error {
// check current hcloud state, validating if ips got assigned / unassigned correctly
if primaryIPv4Two.AssigneeID == srv.ID &&
primaryIPv6One.AssigneeID != srv.ID &&
primaryIPv4One.AssigneeID != srv.ID {
return nil
}
return fmt.Errorf("State is not as expected: \n" +
fmt.Sprintf("primary IP v2 two has assignee id %d which not equals target server id %d",
primaryIPv4Two.AssigneeID, srv.ID) + "\n" +
fmt.Sprintf("primary IP v1 one has assignee id %d and should shouldnt be assigned to server id %d",
primaryIPv4One.AssigneeID, srv.ID) + "\n" +
fmt.Sprintf("primary IP v6 one has assignee id %d and should shouldnt be assigned to server id %d",
primaryIPv6One.AssigneeID, srv.ID))
}),
},
},
})
Expand Down
2 changes: 1 addition & 1 deletion internal/e2etests/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (
TestLoadBalancerType = "lb11"

// TestLocationName is the default location where we execute our tests.
TestLocationName = "fsn1"
TestLocationName = "hel1"
)

func init() {
Expand Down
11 changes: 2 additions & 9 deletions internal/primaryip/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func resourcePrimaryIPDelete(ctx context.Context, d *schema.ResourceData, m inte
}

if assigneeID, ok := d.GetOk("assignee_id"); ok {
shutdown, _, _ := client.Server.Shutdown(ctx, &hcloud.Server{ID: assigneeID.(int)})
shutdown, _, _ := client.Server.Poweroff(ctx, &hcloud.Server{ID: assigneeID.(int)})
if errDiag := watchProgress(ctx, shutdown, client); err != nil {
return errDiag
}
Expand All @@ -269,7 +269,7 @@ func resourcePrimaryIPDelete(ctx context.Context, d *schema.ResourceData, m inte
}
}
err = control.Retry(2*control.DefaultRetries, func() error {
if err := deletePrimaryIP(ctx, client, primaryIPID); err != nil {
if _, err := client.PrimaryIP.Delete(ctx, &hcloud.PrimaryIP{ID: primaryIPID}); err != nil {
return err
}
return nil
Expand Down Expand Up @@ -343,10 +343,3 @@ func watchProgress(ctx context.Context, action *hcloud.Action, client *hcloud.Cl
}
return nil
}

func deletePrimaryIP(ctx context.Context, client *hcloud.Client, primaryIPID int) error {
if _, err := client.PrimaryIP.Delete(ctx, &hcloud.PrimaryIP{ID: primaryIPID}); err != nil {
return err
}
return nil
}
113 changes: 83 additions & 30 deletions internal/server/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ func Resource() *schema.Resource {

Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"ipv4_enabled": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"ipv6_enabled": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"ipv4": {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -302,13 +312,30 @@ func resourceServerCreate(ctx context.Context, d *schema.ResourceData, m interfa

if publicNet, ok := d.GetOk("public_net"); ok {
createPublicNet := hcloud.ServerCreatePublicNet{}
for _, publicNetValue := range publicNet.(*schema.Set).List() {
publicNetEntry := publicNetValue.(map[string]interface{})
if err := toServerPublicNet(publicNetEntry, &createPublicNet); err != nil {
return hcclient.ErrorToDiag(err)
for _, publicNetBlock := range publicNet.(*schema.Set).List() {
publicNetEntry := publicNetBlock.(map[string]interface{})

if enableIPv4, err := toServerPublicNet[bool](publicNetEntry, "ipv4_enabled"); err == nil {
createPublicNet.EnableIPv4 = enableIPv4
}
if enableIPv6, err := toServerPublicNet[bool](publicNetEntry, "ipv6_enabled"); err == nil {
createPublicNet.EnableIPv6 = enableIPv6
}
if ipv4, err := toServerPublicNet[int](publicNetEntry, "ipv4"); err == nil {
createPublicNet.EnableIPv4 = true
createPublicNet.IPv4 = &hcloud.PrimaryIP{ID: ipv4}
}
if ipv6, err := toServerPublicNet[int](publicNetEntry, "ipv6"); err == nil {
createPublicNet.EnableIPv6 = true
createPublicNet.IPv6 = &hcloud.PrimaryIP{ID: ipv6}
}
opts.PublicNet = &createPublicNet
}
opts.PublicNet = &createPublicNet
// if the server has no public net, it has to be created without starting it
onServerCreateWithoutPublicNet(&opts, func(opts *hcloud.ServerCreateOpts) error {
opts.StartAfterCreate = hcloud.Bool(false)
return nil
})
}

res, _, err := c.Server.Create(ctx, opts)
Expand All @@ -333,6 +360,18 @@ func resourceServerCreate(ctx context.Context, d *schema.ResourceData, m interfa
return hcclient.ErrorToDiag(err)
}
}
// if the server was created without public net, the server is now still offline and has to be powered on after
// network assignment
onServerCreateWithoutPublicNet(&opts, func(opts *hcloud.ServerCreateOpts) error {
powerOn, _, err := c.Server.Poweron(ctx, res.Server)
if err != nil {
return err
}
if err := hcclient.WaitForAction(ctx, &c.Action, powerOn); err != nil {
return fmt.Errorf("start server: %v", err)
}
return nil
})
}

backups := d.Get("backups").(bool)
Expand Down Expand Up @@ -584,19 +623,15 @@ func updatePublicNet(ctx context.Context, o interface{}, n interface{}, c *hclou
unassignPrimaryIPIDs := []int{}
assignPrimaryIPIDs := []int{}

// We first prepare all changes to then simply apply them
for _, d := range diffToRemove.List() {
field := d.(map[string]interface{})
r, _ := toPrimaryIPID(field)
unassignPrimaryIPIDs = append(unassignPrimaryIPIDs, r)
fields := d.(map[string]interface{})
unassignPrimaryIPIDs = collectPrimaryIPIDs(fields, unassignPrimaryIPIDs)
}

for _, d := range diffToAdd.List() {
field := d.(map[string]interface{})
r, _ := toPrimaryIPID(field)
assignPrimaryIPIDs = append(assignPrimaryIPIDs, r)
fields := d.(map[string]interface{})
assignPrimaryIPIDs = collectPrimaryIPIDs(fields, assignPrimaryIPIDs)
}

shutdown, _, _ := c.Server.Poweroff(ctx, &hcloud.Server{ID: server.ID})
if err := hcclient.WaitForAction(ctx, &c.Action, shutdown); err != nil {
return hcclient.ErrorToDiag(err)
Expand Down Expand Up @@ -1018,26 +1053,44 @@ func setProtection(ctx context.Context, c *hcloud.Client, server *hcloud.Server,

return nil
}
func toServerPublicNet(field map[string]interface{}, opts *hcloud.ServerCreatePublicNet) error {

func toServerPublicNet[V int | bool](field map[string]interface{}, key string) (V, error) {
var op = "toServerPublicNet"
if ipv4ID, ok := field["ipv4"].(int); ok && ipv4ID != 0 {
opts.EnableIPv4 = true
opts.IPv4 = &hcloud.PrimaryIP{ID: ipv4ID}
return nil
} else if ipv6ID, ok := field["ipv6"].(int); ok && ipv6ID != 0 {
opts.EnableIPv6 = true
opts.IPv6 = &hcloud.PrimaryIP{ID: ipv6ID}
return nil
var valType V
if valType, ok := field[key].(V); ok {
return valType, nil
}
return valType, fmt.Errorf("%s: unable to apply value to public_net values", op)
}

func collectPrimaryIPIDs(primaryIPList map[string]interface{}, list []int) []int {
if r, err := toPublicNetPrimaryIPField[int](primaryIPList, "ipv4"); r != 0 && err == nil {
list = append(list, r)
}
if r, err := toPublicNetPrimaryIPField[int](primaryIPList, "ipv6"); r != 0 && err == nil {
list = append(list, r)
}
return fmt.Errorf("%s: unknown apply to resource", op)
return list
}

func toPrimaryIPID(field map[string]interface{}) (int, error) {
var op = "toPrimaryIPID"
if ipv4ID, ok := field["ipv4"].(int); ok && ipv4ID != 0 {
return ipv4ID, nil
} else if ipv6ID, ok := field["ipv6"].(int); ok && ipv6ID != 0 {
return ipv6ID, nil
func toPublicNetPrimaryIPField[V int | bool](field map[string]interface{}, key string) (V, error) {
var op = "toPublicNetPrimaryIPField"
var fieldValue V
if fieldValue, ok := field[key].(V); ok {
return fieldValue, nil
}
return 0, fmt.Errorf("%s: unknown apply to resource", op)
return fieldValue, fmt.Errorf("%s: field does not contain ID", op)
}

func onServerCreateWithoutPublicNet(opts *hcloud.ServerCreateOpts,
fn func(opts *hcloud.ServerCreateOpts) error) diag.Diagnostics {
if opts.PublicNet != nil {
if opts.PublicNet.EnableIPv6 == false && opts.PublicNet.EnableIPv4 == false {
if err := fn(opts); err != nil {
return hcclient.ErrorToDiag(err)
}
}
return nil
}
return nil
}
2 changes: 1 addition & 1 deletion internal/server/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ type RData struct {
Image string
LocationName string
Datacenter string
PublicNet map[string]string
PublicNet map[string]interface{}
SSHKeys []string
KeepDisk bool
Rescue bool
Expand Down
Loading

0 comments on commit 77d5bde

Please sign in to comment.