diff --git a/go.mod b/go.mod index 8ec0d1fad..876326622 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 675a2ffab..22e731d54 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/e2etests/primaryip/resource_test.go b/internal/e2etests/primaryip/resource_test.go index 2cc6e349c..8f3c0647f 100644 --- a/internal/e2etests/primaryip/resource_test.go +++ b/internal/e2etests/primaryip/resource_test.go @@ -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" @@ -17,7 +19,7 @@ import ( ) const ( - testDatacenter = "fsn1-dc14" + testDatacenter = "hel1-dc2" ) func TestPrimaryIPResource_Basic(t *testing.T) { @@ -84,27 +86,38 @@ 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", @@ -112,8 +125,11 @@ func TestPrimaryIPResource_with_server(t *testing.T) { 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", }, } @@ -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()) @@ -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)) + }), }, }, }) diff --git a/internal/e2etests/testing.go b/internal/e2etests/testing.go index f5006fe62..03ab63bf8 100644 --- a/internal/e2etests/testing.go +++ b/internal/e2etests/testing.go @@ -21,7 +21,7 @@ const ( TestLoadBalancerType = "lb11" // TestLocationName is the default location where we execute our tests. - TestLocationName = "fsn1" + TestLocationName = "hel1" ) func init() { diff --git a/internal/primaryip/resource.go b/internal/primaryip/resource.go index 92a396957..d857d9957 100644 --- a/internal/primaryip/resource.go +++ b/internal/primaryip/resource.go @@ -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 } @@ -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 @@ -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 -} diff --git a/internal/server/resource.go b/internal/server/resource.go index 592fd6628..ec98dbca6 100644 --- a/internal/server/resource.go +++ b/internal/server/resource.go @@ -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, @@ -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) @@ -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) @@ -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) @@ -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 } diff --git a/internal/server/testing.go b/internal/server/testing.go index 30728f7d6..5530ea792 100644 --- a/internal/server/testing.go +++ b/internal/server/testing.go @@ -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 diff --git a/website/docs/r/server.html.md b/website/docs/r/server.html.md index 0eec62699..5d1c58592 100644 --- a/website/docs/r/server.html.md +++ b/website/docs/r/server.html.md @@ -10,7 +10,27 @@ description: |- Provides an Hetzner Cloud server resource. This can be used to create, modify, and delete servers. Servers also support [provisioning](https://www.terraform.io/docs/provisioners/index.html). + + +## Primary IPs When creating a server without linking at least one ´primary_ip´, it automatically creates & assigns two (ipv4 & ipv6). +With the public_net block, you can define if you want to enable or link primary ips. If you don't define this block, two primary ips (ipv4, ipv6) will be created and assigned to the server. + +### Examples + +```hcl +# Assign existing ipv4 only +public_net { + ipv4_enabled = true + ipv4 = hcloud_primary_ip.primary_ip_1.id + ipv6_enabled = false +} +# Assign & create ipv4 & ipv6 +public_net { + ipv4_enabled = true + ipv6_enabled = true +} +``` ## Example Usage @@ -25,18 +45,18 @@ resource "hcloud_server" "node1" { } ``` ```hcl -### Server creation with primary ip +### Server creation with one linked primary ip (ipv4) resource "hcloud_primary_ip" "primary_ip_1" { name = "primary_ip_test" datacenter = "fsn1-dc14" type = "ipv4" assignee_type = "server" auto_delete = true -labels = { -"hallo" : "welt" -} + labels = { + "hallo" : "welt" + } } -// Link a server to a primary IP + resource "hcloud_server" "server_test" { name = "test-server" image = "ubuntu-20.04" @@ -46,12 +66,13 @@ resource "hcloud_server" "server_test" { "test" : "tessst1" } public_net { + ipv4_enabled = true ipv4 = hcloud_primary_ip.primary_ip_1.id + ipv6_enabled = false } } ``` ### Server creation with network - ```hcl resource "hcloud_network" "network" { name = "network" @@ -101,6 +122,7 @@ The following arguments are supported: - `datacenter` - (Optional, string) The datacenter name to create the server in. - `user_data` - (Optional, string) Cloud-Init user data to use during server creation - `ssh_keys` - (Optional, list) SSH key IDs or names which should be injected into the server at creation time +- `public_net` - (Optional, block) In this block you can either enable / disable ipv4 and ipv6 or link existing primary IPs (checkout the examples) - `keep_disk` - (Optional, bool) If true, do not upgrade the disk. This allows downgrading the server type later. - `iso` - (Optional, string) ID or Name of an ISO image to mount. - `rescue` - (Optional, string) Enable and boot in to the specified rescue system. This enables simple installation of custom operating systems. `linux64` `linux32` or `freebsd64`