diff --git a/go.mod b/go.mod index 259b70f3d..654ff8d83 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ 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.7.0 - github.com/hetznercloud/hcloud-go v1.29.1 + github.com/hetznercloud/hcloud-go v1.30.0 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e + golang.org/x/net v0.0.0-20210326060303-6b1517762897 ) go 1.16 diff --git a/go.sum b/go.sum index 15de4846c..fd24bf876 100644 --- a/go.sum +++ b/go.sum @@ -201,8 +201,8 @@ github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0/go.mod h1:grseeRo9g3yNkYW09i github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hetznercloud/hcloud-go v1.29.1 h1:UiV+GZVEOFramb49ASbXfpJGjXa6FmJe3Hh+Ns3RUJ4= -github.com/hetznercloud/hcloud-go v1.29.1/go.mod h1:2C5uMtBiMoFr3m7lBFPf7wXTdh33CevmZpQIIDPGYJI= +github.com/hetznercloud/hcloud-go v1.30.0 h1:Q8Y+YHgum6XvyVfz2IFp2pLWtupEFbykl12D5TwdBig= +github.com/hetznercloud/hcloud-go v1.30.0/go.mod h1:2C5uMtBiMoFr3m7lBFPf7wXTdh33CevmZpQIIDPGYJI= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= diff --git a/hcloud/provider.go b/hcloud/provider.go index ec6992666..7eb9e949b 100644 --- a/hcloud/provider.go +++ b/hcloud/provider.go @@ -8,6 +8,7 @@ import ( "github.com/hetznercloud/terraform-provider-hcloud/internal/firewall" "github.com/hetznercloud/terraform-provider-hcloud/internal/hcclient" + "github.com/hetznercloud/terraform-provider-hcloud/internal/placementgroup" "github.com/hetznercloud/terraform-provider-hcloud/internal/snapshot" @@ -85,6 +86,7 @@ func Provider() *schema.Provider { sshkey.ResourceType: sshkey.Resource(), volume.AttachmentResourceType: volume.AttachmentResource(), volume.ResourceType: volume.Resource(), + placementgroup.ResourceType: placementgroup.Resource(), }, DataSourcesMap: map[string]*schema.Resource{ certificate.DataSourceType: certificate.DataSource(), @@ -103,6 +105,7 @@ func Provider() *schema.Provider { sshkey.DataSourceType: sshkey.DataSource(), sshkey.SSHKeysDataSourceType: sshkey.SSHKeysDataSource(), volume.DataSourceType: volume.DataSource(), + placementgroup.DataSourceType: placementgroup.DataSource(), }, ConfigureContextFunc: providerConfigure, } diff --git a/hcloud/provider_test.go b/hcloud/provider_test.go index ec0e77b1c..2719d77ff 100644 --- a/hcloud/provider_test.go +++ b/hcloud/provider_test.go @@ -11,6 +11,7 @@ import ( "github.com/hetznercloud/terraform-provider-hcloud/internal/loadbalancer" "github.com/hetznercloud/terraform-provider-hcloud/internal/location" "github.com/hetznercloud/terraform-provider-hcloud/internal/network" + "github.com/hetznercloud/terraform-provider-hcloud/internal/placementgroup" "github.com/hetznercloud/terraform-provider-hcloud/internal/rdns" "github.com/hetznercloud/terraform-provider-hcloud/internal/server" "github.com/hetznercloud/terraform-provider-hcloud/internal/servertype" @@ -49,6 +50,7 @@ func TestProvider_Resources(t *testing.T) { sshkey.ResourceType, volume.AttachmentResourceType, volume.ResourceType, + placementgroup.ResourceType, } resources := provider.Resources() @@ -78,6 +80,7 @@ func TestProvider_DataSources(t *testing.T) { sshkey.DataSourceType, sshkey.SSHKeysDataSourceType, volume.DataSourceType, + placementgroup.DataSourceType, } datasources := provider.DataSources() diff --git a/internal/e2etests/placementgroup/data_source_test.go b/internal/e2etests/placementgroup/data_source_test.go new file mode 100644 index 000000000..57d40a969 --- /dev/null +++ b/internal/e2etests/placementgroup/data_source_test.go @@ -0,0 +1,67 @@ +package placementgroup + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hetznercloud/terraform-provider-hcloud/internal/e2etests" + "github.com/hetznercloud/terraform-provider-hcloud/internal/placementgroup" + "github.com/hetznercloud/terraform-provider-hcloud/internal/testsupport" + + "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" +) + +func TestAccHcloudDataSourcePlacementGroupTest(t *testing.T) { + tmplMan := testtemplate.Manager{} + + res := placementgroup.NewRData(t, "basic-placement-group", "spread") + res.SetRName("placement-group-ds-test") + + placementGroupByName := &placementgroup.DData{ + PlacementGroupName: res.TFID() + ".name", + } + placementGroupByName.SetRName("placement_group_by_name") + + placementGroupByID := &placementgroup.DData{ + PlacementGroupID: res.TFID() + ".id", + } + placementGroupByID.SetRName("placement_group_by_id") + + placementGroupBySel := &placementgroup.DData{ + LabelSelector: fmt.Sprintf("key=${%s.labels[\"key\"]}", res.TFID()), + } + placementGroupBySel.SetRName("placement_group_by_sel") + + resource.Test(t, resource.TestCase{ + PreCheck: e2etests.PreCheck(t), + Providers: e2etests.Providers(), + CheckDestroy: testsupport.CheckResourcesDestroyed(placementgroup.ResourceType, placementgroup.ByID(t, nil)), + Steps: []resource.TestStep{ + { + Config: tmplMan.Render(t, + "testdata/r/hcloud_placement_group", res, + ), + }, + { + Config: tmplMan.Render(t, + "testdata/r/hcloud_placement_group", res, + "testdata/d/hcloud_placement_group", placementGroupByName, + "testdata/d/hcloud_placement_group", placementGroupByID, + "testdata/d/hcloud_placement_group", placementGroupBySel, + ), + + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(placementGroupByName.TFID(), + "name", fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt)), + + resource.TestCheckResourceAttr(placementGroupByID.TFID(), + "name", fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt)), + + resource.TestCheckResourceAttr(placementGroupBySel.TFID(), + "name", fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt)), + ), + }, + }, + }) +} diff --git a/internal/e2etests/placementgroup/resource_test.go b/internal/e2etests/placementgroup/resource_test.go new file mode 100644 index 000000000..27c314920 --- /dev/null +++ b/internal/e2etests/placementgroup/resource_test.go @@ -0,0 +1,69 @@ +package placementgroup + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/e2etests" + "github.com/hetznercloud/terraform-provider-hcloud/internal/placementgroup" + "github.com/hetznercloud/terraform-provider-hcloud/internal/testsupport" + "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" +) + +func TestPlacementGroupResource_Basic(t *testing.T) { + var g hcloud.PlacementGroup + + res := placementgroup.NewRData(t, "basic-placement-group", "spread") + resRenamed := &placementgroup.RData{ + Name: res.Name + "-renamed", + Type: "spread", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + resRenamed.SetRName(res.RName()) + + updated := placementgroup.NewRData(t, "basic-placement-group", "spread") + updated.SetRName(res.RName()) + tmplMan := testtemplate.Manager{} + resource.Test(t, resource.TestCase{ + PreCheck: e2etests.PreCheck(t), + Providers: e2etests.Providers(), + CheckDestroy: testsupport.CheckResourcesDestroyed(placementgroup.ResourceType, placementgroup.ByID(t, &g)), + Steps: []resource.TestStep{ + { + // Create a new Placement Group using the required values + // only. + Config: tmplMan.Render(t, "testdata/r/hcloud_placement_group", res), + Check: resource.ComposeTestCheckFunc( + testsupport.CheckResourceExists(res.TFID(), placementgroup.ByID(t, &g)), + resource.TestCheckResourceAttr(res.TFID(), "name", + fmt.Sprintf("basic-placement-group--%d", tmplMan.RandInt)), + resource.TestCheckResourceAttr(res.TFID(), "type", "spread"), + ), + }, + { + // Try to import the newly created Placement Group + ResourceName: res.TFID(), + ImportState: true, + ImportStateVerify: true, + }, + { + // Update the Placement Group created in the previous step by + // setting all optional fields and renaming the volume. + Config: tmplMan.Render(t, + "testdata/r/hcloud_placement_group", resRenamed, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resRenamed.TFID(), "name", + fmt.Sprintf("basic-placement-group-renamed--%d", tmplMan.RandInt)), + resource.TestCheckResourceAttr(resRenamed.TFID(), "labels.key1", "value1"), + resource.TestCheckResourceAttr(resRenamed.TFID(), "labels.key2", "value2"), + ), + }, + }, + }) +} diff --git a/internal/e2etests/server/resource_test.go b/internal/e2etests/server/resource_test.go index aec530106..5273eb395 100644 --- a/internal/e2etests/server/resource_test.go +++ b/internal/e2etests/server/resource_test.go @@ -4,11 +4,13 @@ import ( "crypto/sha1" "encoding/base64" "fmt" + "strconv" "testing" "github.com/hetznercloud/terraform-provider-hcloud/internal/e2etests" "github.com/hetznercloud/terraform-provider-hcloud/internal/firewall" "github.com/hetznercloud/terraform-provider-hcloud/internal/network" + "github.com/hetznercloud/terraform-provider-hcloud/internal/placementgroup" "github.com/hetznercloud/terraform-provider-hcloud/internal/sshkey" "github.com/stretchr/testify/assert" @@ -429,6 +431,52 @@ func TestServerResource_Firewalls(t *testing.T) { }) } +func TestServerResource_PlacementGroup(t *testing.T) { + var ( + pg hcloud.PlacementGroup + srv hcloud.Server + ) + + pgRes := placementgroup.NewRData(t, "server-test", "spread") + + srvRes := &server.RData{ + Name: "server-placement-group", + Type: e2etests.TestServerType, + Image: e2etests.TestImage, + PlacementGroupID: pgRes.TFID() + ".id", + } + srvRes.SetRName("server-placement-group") + + tmplMan := testtemplate.Manager{} + + resource.Test(t, resource.TestCase{ + PreCheck: e2etests.PreCheck(t), + Providers: e2etests.Providers(), + CheckDestroy: testsupport.CheckResourcesDestroyed(server.ResourceType, server.ByID(t, &srv)), + Steps: []resource.TestStep{ + { + // Create a new Server using the required values + // only. + Config: tmplMan.Render(t, + "testdata/r/hcloud_placement_group", pgRes, + "testdata/r/hcloud_server", srvRes, + ), + Check: resource.ComposeTestCheckFunc( + testsupport.CheckResourceExists(srvRes.TFID(), server.ByID(t, &srv)), + testsupport.CheckResourceExists(pgRes.TFID(), placementgroup.ByID(t, &pg)), + resource.TestCheckResourceAttr(srvRes.TFID(), "name", + fmt.Sprintf("server-placement-group--%d", tmplMan.RandInt)), + resource.TestCheckResourceAttr(srvRes.TFID(), "server_type", srvRes.Type), + resource.TestCheckResourceAttr(srvRes.TFID(), "image", srvRes.Image), + testsupport.CheckResourceAttrFunc(srvRes.TFID(), "placement_group_id", func() string { + return strconv.Itoa(pg.ID) + }), + ), + }, + }, + }) +} + func isRecreated(new, old *hcloud.Server) func() error { return func() error { if new.ID == old.ID { diff --git a/internal/placementgroup/data_source.go b/internal/placementgroup/data_source.go new file mode 100644 index 000000000..4219da7ed --- /dev/null +++ b/internal/placementgroup/data_source.go @@ -0,0 +1,108 @@ +package placementgroup + +import ( + "log" + "sort" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/hcclient" + "golang.org/x/net/context" +) + +const DataSourceType = "hcloud_placement_group" + +func DataSource() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceHcloudPlacementGroupRead, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Optional: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + }, + "servers": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + "type": { + Type: schema.TypeString, + Optional: true, + }, + "most_recent": { + Type: schema.TypeBool, + Optional: true, + }, + "with_selector": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceHcloudPlacementGroupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*hcloud.Client) + if id, ok := d.GetOk("id"); ok { + i, _, err := client.PlacementGroup.GetByID(ctx, id.(int)) + if err != nil { + return hcclient.ErrorToDiag(err) + } + if i == nil { + return diag.Errorf("no placement group found with id %d", id) + } + setSchema(d, i) + return nil + } + if name, ok := d.GetOk("name"); ok { + i, _, err := client.PlacementGroup.GetByName(ctx, name.(string)) + if err != nil { + return hcclient.ErrorToDiag(err) + } + if i == nil { + return diag.Errorf("no placement group found with name %v", name) + } + setSchema(d, i) + return nil + } + if selector, ok := d.GetOk("with_selector"); ok { + var allPlacementGroups []*hcloud.PlacementGroup + + opts := hcloud.PlacementGroupListOpts{ListOpts: hcloud.ListOpts{LabelSelector: selector.(string)}} + allPlacementGroups, err := client.PlacementGroup.AllWithOpts(ctx, opts) + if err != nil { + return hcclient.ErrorToDiag(err) + } + if len(allPlacementGroups) == 0 { + return diag.Errorf("no placement group found for selector %q", selector) + } + if len(allPlacementGroups) > 1 { + if _, ok := d.GetOk("most_recent"); !ok { + return diag.Errorf("more than one placement group found for selector %q", selector) + } + sortPlacementGroupListByCreated(allPlacementGroups) + log.Printf("[INFO] %d placement groups found for selector %q, using %d as the most recent one", len(allPlacementGroups), selector, allPlacementGroups[0].ID) + } + setSchema(d, allPlacementGroups[0]) + return nil + } + return diag.Errorf("please specify an id, a name or a selector to lookup the placement group") +} + +func sortPlacementGroupListByCreated(placementGroupList []*hcloud.PlacementGroup) { + sort.Slice(placementGroupList, func(i, j int) bool { + return placementGroupList[i].Created.After(placementGroupList[j].Created) + }) +} diff --git a/internal/placementgroup/resource.go b/internal/placementgroup/resource.go new file mode 100644 index 000000000..4709867d2 --- /dev/null +++ b/internal/placementgroup/resource.go @@ -0,0 +1,200 @@ +package placementgroup + +import ( + "context" + "log" + "strconv" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/hcclient" +) + +const ResourceType = "hcloud_placement_group" + +func Resource() *schema.Resource { + return &schema.Resource{ + CreateContext: create, + ReadContext: read, + UpdateContext: update, + DeleteContext: delete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + }, + "servers": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + placementGroupType := i.(string) + switch hcloud.PlacementGroupType(placementGroupType) { + case hcloud.PlacementGroupTypeSpread: + return nil + default: + return diag.Errorf("%s is not a valid placement group type", placementGroupType) + } + }, + }, + }, + } +} + +func create(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*hcloud.Client) + + opts := hcloud.PlacementGroupCreateOpts{ + Name: d.Get("name").(string), + Type: hcloud.PlacementGroupType(d.Get("type").(string)), + } + if labels, ok := d.GetOk("labels"); ok { + tmpLabels := make(map[string]string) + for k, v := range labels.(map[string]interface{}) { + tmpLabels[k] = v.(string) + } + opts.Labels = tmpLabels + } + + res, _, err := client.PlacementGroup.Create(ctx, opts) + if err != nil { + return hcclient.ErrorToDiag(err) + } + if res.Action != nil { + if err := hcclient.WaitForAction(ctx, &client.Action, res.Action); err != nil { + return hcclient.ErrorToDiag(err) + } + } + + d.SetId(strconv.Itoa(res.PlacementGroup.ID)) + + return read(ctx, d, m) +} + +func read(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*hcloud.Client) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + log.Printf("[WARN] invalid placement group id (%s), removing from state: %v", d.Id(), err) + d.SetId("") + return nil + } + + placementGroup, _, err := client.PlacementGroup.GetByID(ctx, id) + if err != nil { + return hcclient.ErrorToDiag(err) + } + if placementGroup == nil { + log.Printf("[WARN] placement group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + setSchema(d, placementGroup) + return nil +} + +func update(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*hcloud.Client) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + log.Printf("[WARN] invalid placement group id (%s), removing from state: %v", d.Id(), err) + d.SetId("") + return nil + } + + placementGroup, _, err := client.PlacementGroup.GetByID(ctx, id) + if err != nil { + if handleNotFound(err, d) { + return nil + } + return hcclient.ErrorToDiag(err) + } + + d.Partial(true) + + if d.HasChange("name") { + description := d.Get("name").(string) + _, _, err := client.PlacementGroup.Update(ctx, placementGroup, hcloud.PlacementGroupUpdateOpts{ + Name: description, + }) + if err != nil { + if handleNotFound(err, d) { + return nil + } + return hcclient.ErrorToDiag(err) + } + } + + if d.HasChange("labels") { + labels := d.Get("labels") + tmpLabels := make(map[string]string) + for k, v := range labels.(map[string]interface{}) { + tmpLabels[k] = v.(string) + } + _, _, err := client.PlacementGroup.Update(ctx, placementGroup, hcloud.PlacementGroupUpdateOpts{ + Labels: tmpLabels, + }) + if err != nil { + if handleNotFound(err, d) { + return nil + } + return hcclient.ErrorToDiag(err) + } + } + d.Partial(false) + + return read(ctx, d, m) +} + +func delete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*hcloud.Client) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + log.Printf("[WARN] invalid placement group id (%s), removing from state: %v", d.Id(), err) + d.SetId("") + return nil + } + if _, err := client.PlacementGroup.Delete(ctx, &hcloud.PlacementGroup{ID: id}); err != nil { + return hcclient.ErrorToDiag(err) + } + + return nil +} + +func handleNotFound(err error, d *schema.ResourceData) bool { + if hcloud.IsError(err, hcloud.ErrorCodeNotFound) { + log.Printf("[WARN] placement group (%s) not found, removing from state", d.Id()) + d.SetId("") + return true + } + return false +} + +func setSchema(d *schema.ResourceData, v *hcloud.PlacementGroup) { + d.SetId(strconv.Itoa(v.ID)) + d.Set("name", v.Name) + d.Set("labels", v.Labels) + + d.Set("type", v.Type) + d.Set("servers", v.Servers) +} diff --git a/internal/placementgroup/testing.go b/internal/placementgroup/testing.go new file mode 100644 index 000000000..db2f0b4e2 --- /dev/null +++ b/internal/placementgroup/testing.go @@ -0,0 +1,70 @@ +package placementgroup + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" +) + +// ByID returns a function that obtains a placement_group by its ID. +func ByID(t *testing.T, placementGroup *hcloud.PlacementGroup) func(*hcloud.Client, int) bool { + return func(c *hcloud.Client, id int) bool { + found, _, err := c.PlacementGroup.GetByID(context.Background(), id) + if err != nil { + t.Fatalf("find placement group %d: %v", id, err) + } + if found == nil { + return false + } + if placementGroup != nil { + *placementGroup = *found + } + return true + } +} + +// DData defines the fields for the "testdata/d/hcloud_placement_group" +// template. +type DData struct { + testtemplate.DataCommon + + PlacementGroupID string + PlacementGroupName string + LabelSelector string +} + +// TFID returns the data source identifier. +func (d *DData) TFID() string { + return fmt.Sprintf("data.%s.%s", DataSourceType, d.RName()) +} + +// RData defines the fields for the "testdata/r/hcloud_placement_group" +// template. +type RData struct { + testtemplate.DataCommon + + Name string + Labels map[string]string + Type string +} + +// TFID returns the resource identifier. +func (d *RData) TFID() string { + return fmt.Sprintf("%s.%s", ResourceType, d.RName()) +} + +func NewRData(t *testing.T, name string, groupType string) *RData { + rInt := acctest.RandInt() + r := &RData{ + Name: name, + Type: groupType, + Labels: map[string]string{"key": strconv.Itoa(rInt)}, + } + r.SetRName(name) + return r +} diff --git a/internal/server/data_source.go b/internal/server/data_source.go index 7189b4af1..8911e77aa 100644 --- a/internal/server/data_source.go +++ b/internal/server/data_source.go @@ -103,6 +103,10 @@ func DataSource() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeInt}, }, + "placement_group": { + Type: schema.TypeString, + Optional: true, + }, }, } } diff --git a/internal/server/resource.go b/internal/server/resource.go index c102faf42..4e2fcbd2d 100644 --- a/internal/server/resource.go +++ b/internal/server/resource.go @@ -168,6 +168,10 @@ func Resource() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeInt}, }, + "placement_group_id": { + Type: schema.TypeInt, + Optional: true, + }, }, } } @@ -231,6 +235,15 @@ func resourceServerCreate(ctx context.Context, d *schema.ResourceData, m interfa } } + if placementGroupID, ok := d.GetOk("placement_group_id"); ok { + placementGroup, err := getPlacementGroup(ctx, c, placementGroupID.(int)) + if err != nil { + return hcclient.ErrorToDiag(err) + } + + opts.PlacementGroup = placementGroup + } + res, _, err := c.Server.Create(ctx, opts) if err != nil { return hcclient.ErrorToDiag(err) @@ -463,6 +476,13 @@ func resourceServerUpdate(ctx context.Context, d *schema.ResourceData, m interfa } } + if d.HasChange("placement_group") { + placementGroupID := d.Get("placement_group").(int) + if err := setPlacementGroup(ctx, c, server, placementGroupID); err != nil { + return hcclient.ErrorToDiag(err) + } + } + d.Partial(false) return resourceServerRead(ctx, d, m) } @@ -742,6 +762,10 @@ func setServerSchema(d *schema.ResourceData, s *hcloud.Server) { if _, ok := d.GetOk("network"); ok { d.Set("network", networkToTerraformNetworks(s.PrivateNet)) } + + if s.PlacementGroup != nil { + d.Set("placement_group_id", s.PlacementGroup.ID) + } } func networkToTerraformNetworks(privateNetworks []hcloud.ServerPrivateNet) []map[string]interface{} { @@ -760,3 +784,45 @@ func networkToTerraformNetworks(privateNetworks []hcloud.ServerPrivateNet) []map } return tfPrivateNetworks } + +func getPlacementGroup(ctx context.Context, c *hcloud.Client, id int) (*hcloud.PlacementGroup, error) { + placementGroup, _, err := c.PlacementGroup.GetByID(ctx, id) + if err != nil { + return nil, err + } + + if placementGroup == nil { + return nil, fmt.Errorf("placement group not found: %d", id) + } + + return placementGroup, nil +} + +func setPlacementGroup(ctx context.Context, c *hcloud.Client, server *hcloud.Server, id int) error { + if server.PlacementGroup != nil { + action, _, err := c.Server.RemoveFromPlacementGroup(ctx, server) + if err != nil { + return err + } + if err := hcclient.WaitForAction(ctx, &c.Action, action); err != nil { + return err + } + } + + if id != 0 { + placementGroup, err := getPlacementGroup(ctx, c, id) + if err != nil { + return err + } + + action, _, err := c.Server.AddToPlacementGroup(ctx, server, placementGroup) + if err != nil { + return err + } + if err := hcclient.WaitForAction(ctx, &c.Action, action); err != nil { + return err + } + } + + return nil +} diff --git a/internal/server/testing.go b/internal/server/testing.go index b97adc6a5..487ea9c86 100644 --- a/internal/server/testing.go +++ b/internal/server/testing.go @@ -77,21 +77,22 @@ func (d *DData) TFID() string { type RData struct { testtemplate.DataCommon - Name string - Type string - Image string - LocationName string - DataCenter string - SSHKeys []string - KeepDisk bool - Rescue bool - Backups bool - ISO string - Labels map[string]string - UserData string - Network RDataInlineNetwork - FirewallIDs []string - DependsOn []string + Name string + Type string + Image string + LocationName string + DataCenter string + SSHKeys []string + KeepDisk bool + Rescue bool + Backups bool + ISO string + Labels map[string]string + UserData string + Network RDataInlineNetwork + FirewallIDs []string + DependsOn []string + PlacementGroupID string } // RDataInlineNetwork defines the information required to attach a server diff --git a/internal/testdata/d/hcloud_placement_group.tf.tmpl b/internal/testdata/d/hcloud_placement_group.tf.tmpl new file mode 100644 index 000000000..5acd4bd53 --- /dev/null +++ b/internal/testdata/d/hcloud_placement_group.tf.tmpl @@ -0,0 +1,7 @@ +{{- /* vim: set ft=terraform: */ -}} + +data "hcloud_placement_group" "{{ .RName }}" { + {{ if .PlacementGroupID -}} id = {{ .PlacementGroupID }}{{ end -}} + {{ if .PlacementGroupName -}} name = {{ .PlacementGroupName }}{{ end -}} + {{ if .LabelSelector -}} with_selector = "{{ .LabelSelector }}"{{ end }} +} diff --git a/internal/testdata/r/hcloud_placement_group.tf.tmpl b/internal/testdata/r/hcloud_placement_group.tf.tmpl new file mode 100644 index 000000000..00c80742d --- /dev/null +++ b/internal/testdata/r/hcloud_placement_group.tf.tmpl @@ -0,0 +1,13 @@ +{{- /* vim: set ft=terraform: */ -}} + +resource "hcloud_placement_group" "{{ .RName }}" { + name = "{{ .Name }}--{{ .RInt }}" + type = "{{ .Type }}" + {{- if .Labels }} + labels = { + {{- range $k,$v := .Labels }} + {{ $k }} = "{{ $v }}" + {{- end }} + } + {{- end }} +} \ No newline at end of file diff --git a/internal/testdata/r/hcloud_server.tf.tmpl b/internal/testdata/r/hcloud_server.tf.tmpl index 07b0b2a3d..285bc85dd 100644 --- a/internal/testdata/r/hcloud_server.tf.tmpl +++ b/internal/testdata/r/hcloud_server.tf.tmpl @@ -64,4 +64,8 @@ resource "hcloud_server" "{{ .RName }}" { {{- if .DependsOn }} depends_on = [{{ StringsJoin .DependsOn ", " }}] {{ end }} + + {{- if .PlacementGroupID }} + placement_group_id = {{ .PlacementGroupID }} + {{ end }} } diff --git a/website/docs/d/placementgroup.html.md b/website/docs/d/placementgroup.html.md new file mode 100644 index 000000000..21d43cba5 --- /dev/null +++ b/website/docs/d/placementgroup.html.md @@ -0,0 +1,34 @@ +--- +layout: "hcloud" +page_title: "Hetzner Cloud: hcloud_placement_group" +sidebar_current: "docs-hcloud-datasource-placement-group" +description: |- +Provides details about a specific Hetzner Cloud Placement Group. +--- + +# hcloud_placement_group + +Provides details about a specific Hetzner Cloud Placement Group. + +```hcl +data "hcloud_placement_group" "sample_placement_group_1" { + name = "sample-placement-group-1" +} + +data "hcloud_placement_group" "sample_placement_group_2" { + id = "4711" +} +``` + +## Argument Reference + +- `id` - ID of the placement group. +- `name` - Name of the placement group. +- `with_selector` - (Optional, string) [Label selector](https://docs.hetzner.cloud/#overview-label-selector) + +## Attribute Reference + +- `id` - (int) Unique ID of the Placement Group. +- `name` - (string) Name of the Placement Group. +- `type` - (string) Type of the Placement Group. +- `labels` - (map) User-defined labels (key-value pairs) diff --git a/website/docs/d/server.html.md b/website/docs/d/server.html.md index fc8d5fee9..91c32348b 100644 --- a/website/docs/d/server.html.md +++ b/website/docs/d/server.html.md @@ -44,3 +44,4 @@ data "hcloud_server" "s_3" { - `status` - (string) The status of the server. - `labels` - (map) User-defined labels (key-value pairs) - `firewall_ids` - (Optional, list) Firewall IDs the server is attached to. +- `placement_group_id` - (Optional, string) Placement Group ID the server is assigned to. diff --git a/website/docs/r/placementgroup.html.md b/website/docs/r/placementgroup.html.md new file mode 100644 index 000000000..1f6993e67 --- /dev/null +++ b/website/docs/r/placementgroup.html.md @@ -0,0 +1,51 @@ +--- +layout: "hcloud" +page_title: "Hetzner Cloud: hcloud_placement_group" +sidebar_current: "docs-hcloud-placement-group" +description: |- +Provides a Hetzner Cloud Placement Group to represent a Placement Group in the Hetzner Cloud. +--- + +# hcloud_placement_group + +Provides a Hetzner Cloud Placement Group to represent a Placement Group in the Hetzner Cloud. + +## Example Usage + +```hcl +resource "hcloud_placement_group" "my-placement-group" { + name = "my-placement-group" + type = "spread" + labels = { + key = "value" + } +} + +resource "hcloud_server" "node1" { + name = "node1" + image = "debian-9" + server_type = "cx11" + placement_group_id = hcloud_placement_group.my-placement-group.id +} +``` + +## Argument Reference + +- `name` - (Optional, string) Name of the Placement Group. +- `type` - (Required, string) Type of the Placement Group. +- `labels` - (Optional, map) User-defined labels (key-value pairs) should be created with. + +## Attributes Reference + +- `id` - (int) Unique ID of the Placement Group. +- `name` - (string) Name of the Placement Group. +- `type` - (string) Type of the Placement Group. +- `labels` - (map) User-defined labels (key-value pairs) + +## Import + +Placement Groups can be imported using its `id`: + +``` +terraform import hcloud_placement_group.my-placement-group +``` diff --git a/website/docs/r/server.html.md b/website/docs/r/server.html.md index 98c4d415a..323b4a5c1 100644 --- a/website/docs/r/server.html.md +++ b/website/docs/r/server.html.md @@ -81,6 +81,7 @@ The following arguments are supported: - `backups` - (Optional, boolean) Enable or disable backups. - `firewall_ids` - (Optional, list) Firewall IDs the server should be attached to on creation. - `network` - (Optional) Network the server should be attached to on creation. (Can be specified multiple times) +- `placement_group_id` - (Optional, string) Placement Group ID the server added to on creation. `network` support the following fields: - `network_id` - (Required, int) ID of the network @@ -115,6 +116,7 @@ The following attributes are exported: to the respective subnetwork. See examples. - `firewall_ids` - (Optional, list) Firewall IDs the server is attached to. - `network` - (Optional, list) Network the server should be attached to on creation. (Can be specified multiple times) +- `placement_group_id` - (Optional, string) Placement Group ID the server is assigned to. a single entry in `network` support the following fields: - `network_id` - (Required, int) ID of the network diff --git a/website/hcloud.erb b/website/hcloud.erb index d4cc95e95..999c8a4ee 100644 --- a/website/hcloud.erb +++ b/website/hcloud.erb @@ -60,6 +60,9 @@ > hcloud_volume + > + hcloud_placement_group + > @@ -116,6 +119,9 @@ > hcloud_rdns + > + hcloud_placement_group +