From 5beec558d570b4a4b78c5dfdf9d5bd218e280243 Mon Sep 17 00:00:00 2001 From: Dmitrii Chudinov Date: Mon, 26 Feb 2024 13:13:14 +0300 Subject: [PATCH] Added entities for work with DNS v2 API (#249) Added selectel_domains_zone_v2 resource. Added selectel_domains_rrset_v2 resource. --- go.mod | 5 +- go.sum | 14 +- selectel/domains_v2.go | 157 ++++++++++++ selectel/domains_v2_test.go | 166 +++++++++++++ .../import_selectel_domains_rrset_v2_test.go | 77 ++++++ .../import_selectel_domains_zone_v2_test.go | 51 ++++ selectel/messages.go | 4 + selectel/provider.go | 4 + selectel/provider_test.go | 7 + .../resource_selectel_domains_rrset_v2.go | 234 ++++++++++++++++++ ...resource_selectel_domains_rrset_v2_test.go | 100 ++++++++ selectel/resource_selectel_domains_zone_v2.go | 214 ++++++++++++++++ .../resource_selectel_domains_zone_v2_test.go | 92 +++++++ .../docs/d/domains_domain_v1.html.markdown | 6 +- .../docs/r/domains_domain_v1.html.markdown | 5 +- .../docs/r/domains_record_v1.html.markdown | 19 +- website/docs/r/domains_rrset_v2.html.markdown | 224 +++++++++++++++++ website/docs/r/domains_zone_v2.html.markdown | 66 +++++ website/selectel.erb | 6 + 19 files changed, 1434 insertions(+), 17 deletions(-) create mode 100644 selectel/domains_v2.go create mode 100644 selectel/domains_v2_test.go create mode 100644 selectel/import_selectel_domains_rrset_v2_test.go create mode 100644 selectel/import_selectel_domains_zone_v2_test.go create mode 100644 selectel/resource_selectel_domains_rrset_v2.go create mode 100644 selectel/resource_selectel_domains_rrset_v2_test.go create mode 100644 selectel/resource_selectel_domains_zone_v2.go create mode 100644 selectel/resource_selectel_domains_zone_v2_test.go create mode 100644 website/docs/r/domains_rrset_v2.html.markdown create mode 100644 website/docs/r/domains_zone_v2.html.markdown diff --git a/go.mod b/go.mod index 4d2e1cd3..59282a64 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 github.com/selectel/craas-go v0.3.0 github.com/selectel/dbaas-go v0.10.0 - github.com/selectel/domains-go v0.5.0 + github.com/selectel/domains-go v1.0.2 github.com/selectel/go-selvpcclient/v3 v3.1.1 github.com/selectel/mks-go v0.12.0 - github.com/stretchr/testify v1.7.2 + github.com/stretchr/testify v1.8.4 ) require ( @@ -51,6 +51,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect diff --git a/go.sum b/go.sum index 91e70786..10a05b27 100644 --- a/go.sum +++ b/go.sum @@ -104,7 +104,7 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 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/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= @@ -153,8 +153,8 @@ github.com/selectel/craas-go v0.3.0 h1:tXiw3LNN+ZVV0wZdeBBXX6u8kMuA5PV/5W1uYqV0y github.com/selectel/craas-go v0.3.0/go.mod h1:9RAUn9PdMITP4I3GAade6v2hjB2j3lo3J2dDlG5SLYE= github.com/selectel/dbaas-go v0.10.0 h1:iY2Q7PY9ICoWBDtni+6oWGR2uAWodER0K2zchNLIOl4= github.com/selectel/dbaas-go v0.10.0/go.mod h1:uyPhqmcvdmKBt9yWhogoSQgWkcZ9QgVlbgaERdSdAfk= -github.com/selectel/domains-go v0.5.0 h1:RCrWY/9KHVtfdA+X8M+DDzsjILxFChhY70HnJEu1Y2U= -github.com/selectel/domains-go v0.5.0/go.mod h1:AhXhwyMSTkpEWFiBLUvzFP76W+WN+ZblwmjLJLt7y58= +github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW0a4U= +github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM= github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= github.com/selectel/mks-go v0.12.0 h1:nLWHK8BXkhFlXvjFqf7WRrdAfvmrOhQzDSLx7BGa6aM= @@ -164,12 +164,18 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= diff --git a/selectel/domains_v2.go b/selectel/domains_v2.go new file mode 100644 index 00000000..062055c8 --- /dev/null +++ b/selectel/domains_v2.go @@ -0,0 +1,157 @@ +package selectel + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + domainsV2 "github.com/selectel/domains-go/pkg/v2" +) + +var ErrProjectIDNotSetupForDNSV2 = errors.New("env variable SEL_PROJECT_ID or variable project_id must be set for the dns v2") + +func getDomainsV2Client(d *schema.ResourceData, meta interface{}) (domainsV2.DNSClient[domainsV2.Zone, domainsV2.RRSet], error) { + config := meta.(*Config) + projectID := d.Get("project_id").(string) + selvpcClient, err := config.GetSelVPCClientWithProjectScope(projectID) + if err != nil { + return nil, fmt.Errorf("can't get selvpc client for domains v2: %w", err) + } + + httpClient := &http.Client{} + userAgent := "terraform-provider-selectel" + defaultAPIURL := "https://api.selectel.ru/domains/v2" + hdrs := http.Header{} + hdrs.Add("X-Auth-Token", selvpcClient.GetXAuthToken()) + hdrs.Add("User-Agent", userAgent) + domainsClient := domainsV2.NewClient(defaultAPIURL, httpClient, hdrs) + + return domainsClient, nil +} + +func getZoneByName(ctx context.Context, client domainsV2.DNSClient[domainsV2.Zone, domainsV2.RRSet], zoneName string) (*domainsV2.Zone, error) { + optsForSearchZone := map[string]string{ + "filter": zoneName, + "limit": "1000", + "offset": "0", + } + r, err := regexp.Compile(fmt.Sprintf("^%s.?", zoneName)) + if err != nil { + return nil, err + } + + for { + zones, err := client.ListZones(ctx, &optsForSearchZone) + if err != nil { + return nil, err + } + + for _, zone := range zones.GetItems() { + if r.MatchString(zone.Name) { + return zone, nil + } + } + optsForSearchZone["offset"] = strconv.Itoa(zones.GetNextOffset()) + if zones.GetNextOffset() == 0 { + break + } + } + + return nil, errGettingObject(objectZone, zoneName, ErrZoneNotFound) +} + +func getRRSetByNameAndType(ctx context.Context, client domainsV2.DNSClient[domainsV2.Zone, domainsV2.RRSet], zoneID, rrsetName, rrsetType string) (*domainsV2.RRSet, error) { + optsForSearchRRSet := map[string]string{ + "name": rrsetName, + "rrset_types": rrsetType, + "limit": "1000", + "offset": "0", + } + + r, err := regexp.Compile(fmt.Sprintf("^%s.?", rrsetName)) + if err != nil { + return nil, errGettingObject(objectRRSet, rrsetName, err) + } + + for { + rrsets, err := client.ListRRSets(ctx, zoneID, &optsForSearchRRSet) + if err != nil { + return nil, errGettingObject(objectRRSet, rrsetName, err) + } + for _, rrset := range rrsets.GetItems() { + if r.MatchString(rrset.Name) && string(rrset.Type) == rrsetType { + return rrset, nil + } + } + optsForSearchRRSet["offset"] = strconv.Itoa(rrsets.GetNextOffset()) + if rrsets.GetNextOffset() == 0 { + break + } + } + + return nil, errGettingObject(objectRRSet, fmt.Sprintf("Name: %s. Type: %s.", rrsetName, rrsetType), ErrRRSetNotFound) +} + +func setZoneToResourceData(d *schema.ResourceData, zone *domainsV2.Zone) error { + d.SetId(zone.ID) + d.Set("name", zone.Name) + d.Set("comment", zone.Comment) + d.Set("created_at", zone.CreatedAt.Format(time.RFC3339)) + d.Set("updated_at", zone.UpdatedAt.Format(time.RFC3339)) + d.Set("delegation_checked_at", zone.DelegationCheckedAt.Format(time.RFC3339)) + d.Set("last_check_status", zone.LastCheckStatus) + d.Set("last_delegated_at", zone.LastDelegatedAt.Format(time.RFC3339)) + d.Set("project_id", strings.ReplaceAll(zone.ProjectID, "-", "")) + d.Set("disabled", zone.Disabled) + + return nil +} + +func setRRSetToResourceData(d *schema.ResourceData, rrset *domainsV2.RRSet) error { + d.SetId(rrset.ID) + d.Set("name", rrset.Name) + d.Set("comment", rrset.Comment) + d.Set("managed_by", rrset.ManagedBy) + d.Set("ttl", rrset.TTL) + d.Set("type", rrset.Type) + d.Set("zone_id", rrset.ZoneID) + d.Set("records", generateSetFromRecords(rrset.Records)) + + return nil +} + +// generateSetFromRecords - generate terraform TypeList from records in RRSet. +func generateSetFromRecords(records []domainsV2.RecordItem) []interface{} { + recordsAsList := []interface{}{} + for _, record := range records { + recordsAsList = append(recordsAsList, map[string]interface{}{ + "content": record.Content, + "disabled": record.Disabled, + }) + } + + return recordsAsList +} + +// generateRecordsFromSet - generate records for RRSet from terraform TypeList. +func generateRecordsFromSet(recordsSet *schema.Set) []domainsV2.RecordItem { + records := []domainsV2.RecordItem{} + for _, recordItem := range recordsSet.List() { + record, isOk := recordItem.(map[string]interface{}) + if !isOk { + continue + } + records = append(records, domainsV2.RecordItem{ + Content: record["content"].(string), + Disabled: record["disabled"].(bool), + }) + } + + return records +} diff --git a/selectel/domains_v2_test.go b/selectel/domains_v2_test.go new file mode 100644 index 00000000..bcdffc42 --- /dev/null +++ b/selectel/domains_v2_test.go @@ -0,0 +1,166 @@ +package selectel + +import ( + "context" + "fmt" + "net/http" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + domainsV2 "github.com/selectel/domains-go/pkg/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func getDomainsV2ClientTest(rs *terraform.ResourceState, testAccProvider *schema.Provider) (domainsV2.DNSClient[domainsV2.Zone, domainsV2.RRSet], error) { + config := testAccProvider.Meta().(*Config) + projectID, ok := rs.Primary.Attributes["project_id"] + if !ok { + return nil, ErrProjectIDNotSetupForDNSV2 + } + selvpcClient, err := config.GetSelVPCClientWithProjectScope(projectID) + if err != nil { + return nil, fmt.Errorf("can't get selvpc client for domains v2: %w", err) + } + + httpClient := &http.Client{} + userAgent := "terraform-provider-selectel" + defaultAPIURL := "https://api.selectel.ru/domains/v2" + hdrs := http.Header{} + hdrs.Add("X-Auth-Token", selvpcClient.GetXAuthToken()) + hdrs.Add("User-Agent", userAgent) + domainsClient := domainsV2.NewClient(defaultAPIURL, httpClient, hdrs) + + return domainsClient, nil +} + +type mockedDNSv2Client struct { + mock.Mock + domainsV2.Client +} + +func (client *mockedDNSv2Client) ListZones(ctx context.Context, opts *map[string]string) (domainsV2.Listable[domainsV2.Zone], error) { + args := client.Called(ctx, opts) + zones := args.Get(0).(domainsV2.Listable[domainsV2.Zone]) + err := args.Error(1) + + return zones, err +} + +func (client *mockedDNSv2Client) ListRRSets(ctx context.Context, zoneID string, opts *map[string]string) (domainsV2.Listable[domainsV2.RRSet], error) { + args := client.Called(ctx, zoneID, opts) + rrsets := args.Get(0).(domainsV2.Listable[domainsV2.RRSet]) + err := args.Error(1) + + return rrsets, err +} + +func TestGetZoneByName_whenNeededZoneInResponseWithOffset(t *testing.T) { + nameForSearch := "test.xyz." + correctIDForSearch := "mocked-uuid-2" + + mDNSClient := new(mockedDNSv2Client) + ctx := context.Background() + nextOffset := 3 + opts1 := &map[string]string{ + "filter": nameForSearch, + "limit": "1000", + "offset": "0", + } + opts2 := &map[string]string{ + "filter": nameForSearch, + "limit": "1000", + "offset": strconv.Itoa(nextOffset), + } + incorrectNameForSearch := "a." + nameForSearch + incorrectIDForSearch := "mocked-uuid-1" + zonesWithNextOffset := domainsV2.Listable[domainsV2.Zone](domainsV2.List[domainsV2.Zone]{ + Count: 1, + NextOffset: nextOffset, + Items: []*domainsV2.Zone{ + { + ID: incorrectIDForSearch, + Name: incorrectNameForSearch, + }, + }, + }) + mDNSClient.On("ListZones", ctx, opts1).Return(zonesWithNextOffset, nil) + zonesWithoutNextOffset := domainsV2.Listable[domainsV2.Zone](domainsV2.List[domainsV2.Zone]{ + Count: 1, + NextOffset: 0, + Items: []*domainsV2.Zone{ + { + ID: correctIDForSearch, + Name: nameForSearch, + }, + }, + }) + mDNSClient.On("ListZones", ctx, opts2).Return(zonesWithoutNextOffset, nil) + + zone, err := getZoneByName(ctx, mDNSClient, nameForSearch) + + assert.NoError(t, err) + + assert.NotNil(t, zone) + assert.Equal(t, correctIDForSearch, zone.ID) + assert.Equal(t, nameForSearch, zone.Name) +} + +func TestGetRRSetByNameAndType_whenNeededRRSetInResponseWithOffset(t *testing.T) { + rrsetNameForSearch := "test.xyz." + rrsetTypeForSearch := "A" + correctIDForSearch := "mocked-uuid-2" + mockedZoneID := "mopcked-zone-id" + mDNSClient := new(mockedDNSv2Client) + ctx := context.Background() + nextOffset := 3 + opts1 := &map[string]string{ + "name": rrsetNameForSearch, + "rrset_types": rrsetTypeForSearch, + "limit": "1000", + "offset": "0", + } + opts2 := &map[string]string{ + "name": rrsetNameForSearch, + "rrset_types": rrsetTypeForSearch, + "limit": "1000", + "offset": strconv.Itoa(nextOffset), + } + incorrectNameForSearch := "a." + rrsetNameForSearch + incorrectIDForSearch := "mocked-uuid-1" + rrsetWithNextOffset := domainsV2.Listable[domainsV2.RRSet](domainsV2.List[domainsV2.RRSet]{ + Count: 1, + NextOffset: nextOffset, + Items: []*domainsV2.RRSet{ + { + ID: incorrectIDForSearch, + Name: incorrectNameForSearch, + Type: domainsV2.RecordType(rrsetTypeForSearch), + }, + }, + }) + mDNSClient.On("ListRRSets", ctx, mockedZoneID, opts1).Return(rrsetWithNextOffset, nil) + rrsetsWithoutNextOffset := domainsV2.Listable[domainsV2.RRSet](domainsV2.List[domainsV2.RRSet]{ + Count: 1, + NextOffset: 0, + Items: []*domainsV2.RRSet{ + { + ID: correctIDForSearch, + Name: rrsetNameForSearch, + Type: domainsV2.RecordType(rrsetTypeForSearch), + }, + }, + }) + mDNSClient.On("ListRRSets", ctx, mockedZoneID, opts2).Return(rrsetsWithoutNextOffset, nil) + + rrset, err := getRRSetByNameAndType(ctx, mDNSClient, mockedZoneID, rrsetNameForSearch, rrsetTypeForSearch) + + assert.NoError(t, err) + + assert.NotNil(t, rrset) + assert.Equal(t, correctIDForSearch, rrset.ID) + assert.Equal(t, rrsetNameForSearch, rrset.Name) + assert.Equal(t, rrsetTypeForSearch, string(rrset.Type)) +} diff --git a/selectel/import_selectel_domains_rrset_v2_test.go b/selectel/import_selectel_domains_rrset_v2_test.go new file mode 100644 index 00000000..ae7aecf8 --- /dev/null +++ b/selectel/import_selectel_domains_rrset_v2_test.go @@ -0,0 +1,77 @@ +package selectel + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + domainsV2 "github.com/selectel/domains-go/pkg/v2" +) + +func TestAccDomainsRRSetV2ImportBasic(t *testing.T) { + projectID := os.Getenv("SEL_PROJECT_ID") + testZoneName := fmt.Sprintf("%s.xyz.", acctest.RandomWithPrefix("tf-acc")) + testRRSetName := fmt.Sprintf("%[1]s.%[2]s", acctest.RandomWithPrefix("tf-acc"), testZoneName) + testRRSetType := domainsV2.TXT + testRRSetTTL := 60 + testRRSetContent := fmt.Sprintf("\"%[1]s\"", acctest.RandString(16)) + fullResourceName := fmt.Sprintf("selectel_domains_rrset_v2.%[1]s", resourceRRSetName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheckWithProjectID(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckDomainsV2RRSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDomainsRRSetV2WithZoneWithoutProjectBasic( + projectID, + resourceRRSetName, testRRSetName, string(testRRSetType), testRRSetContent, testRRSetTTL, + resourceZoneName, testZoneName, + ), + }, + { + ImportStateIdFunc: getTestRRSetIDForImport, + ResourceName: fullResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func getTestRRSetIDForImport(s *terraform.State) (string, error) { + resourceZoneFullName := "selectel_domains_zone_v2.zone_tf_acc_test_1" + resourceRRSetFullName := "selectel_domains_rrset_v2.rrset_tf_acc_test_1" + resourceZone, ok := s.RootModule().Resources[resourceZoneFullName] + if !ok { + return "", fmt.Errorf("Not found zone: %s", resourceZoneFullName) + } + resourceRRSet, ok := s.RootModule().Resources[resourceRRSetFullName] + if !ok { + return "", fmt.Errorf("Not found rrset: %s", resourceRRSetFullName) + } + + return fmt.Sprintf("%s/%s/%s", + resourceZone.Primary.Attributes["name"], + resourceRRSet.Primary.Attributes["name"], + resourceRRSet.Primary.Attributes["type"], + ), nil +} + +func testAccDomainsRRSetV2WithZoneWithoutProjectBasic(projectID, resourceRRSetName, rrsetName, rrsetType, rrsetContent string, ttl int, resourceZoneName, zoneName string) string { + return fmt.Sprintf(` + %[8]s + resource "selectel_domains_rrset_v2" %[1]q { + name = %[2]q + project_id = %[7]q + type = %[3]q + ttl = %[4]d + zone_id = selectel_domains_zone_v2.%[6]s.id + records { + content = %[5]q + disabled = false + } + }`, resourceRRSetName, rrsetName, rrsetType, ttl, rrsetContent, resourceZoneName, projectID, testAccDomainsZoneV2WithoutProjectBasic(projectID, resourceZoneName, zoneName)) +} diff --git a/selectel/import_selectel_domains_zone_v2_test.go b/selectel/import_selectel_domains_zone_v2_test.go new file mode 100644 index 00000000..1ee1695d --- /dev/null +++ b/selectel/import_selectel_domains_zone_v2_test.go @@ -0,0 +1,51 @@ +package selectel + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDomainsZoneV2ImportBasic(t *testing.T) { + projectID := os.Getenv("SEL_PROJECT_ID") + fullResourceName := fmt.Sprintf("selectel_domains_zone_v2.%[1]s", resourceZoneName) + testZoneName := fmt.Sprintf("%s.xyz.", acctest.RandomWithPrefix("tf-acc")) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheckWithProjectID(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckDomainsV2ZoneDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDomainsZoneV2WithoutProjectBasic(projectID, resourceZoneName, testZoneName), + }, + { + ResourceName: fullResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getTestZoneIDForImport, + }, + }, + }) +} + +func getTestZoneIDForImport(s *terraform.State) (string, error) { + resourceZoneFullName := "selectel_domains_zone_v2.zone_tf_acc_test_1" + resourceZone, ok := s.RootModule().Resources[resourceZoneFullName] + if !ok { + return "", fmt.Errorf("Not found zone: %s", resourceZoneFullName) + } + + return resourceZone.Primary.Attributes["name"], nil +} + +func testAccDomainsZoneV2WithoutProjectBasic(projectID, resourceName, zoneName string) string { + return fmt.Sprintf(` + resource "selectel_domains_zone_v2" %[2]q { + name = %[3]q + project_id = %[1]q + }`, projectID, resourceName, zoneName) +} diff --git a/selectel/messages.go b/selectel/messages.go index fd8b31b4..786e3833 100644 --- a/selectel/messages.go +++ b/selectel/messages.go @@ -17,3 +17,7 @@ func msgUpdate(object, id string, options interface{}) string { func msgDelete(object, id string) string { return fmt.Sprintf("[DEBUG] Deleting %s '%s'", object, id) } + +func msgImport(object, id string) string { + return fmt.Sprintf("[DEBUG] Importing %s '%s'", object, id) +} diff --git a/selectel/provider.go b/selectel/provider.go index ab346f9e..b6d424d9 100644 --- a/selectel/provider.go +++ b/selectel/provider.go @@ -26,6 +26,8 @@ const ( objectNodegroup = "nodegroup" objectDomain = "domain" objectRecord = "record" + objectZone = "zone" + objectRRSet = "rrset" objectDatastore = "datastore" objectDatabase = "database" objectGrant = "grant" @@ -125,6 +127,8 @@ func Provider() *schema.Provider { "selectel_mks_nodegroup_v1": resourceMKSNodegroupV1(), "selectel_domains_domain_v1": resourceDomainsDomainV1(), "selectel_domains_record_v1": resourceDomainsRecordV1(), + "selectel_domains_zone_v2": resourceDomainsZoneV2(), + "selectel_domains_rrset_v2": resourceDomainsRRSetV2(), "selectel_dbaas_datastore_v1": resourceDBaaSDatastoreV1(), // DEPRECATED "selectel_dbaas_postgresql_datastore_v1": resourceDBaaSPostgreSQLDatastoreV1(), "selectel_dbaas_mysql_datastore_v1": resourceDBaaSMySQLDatastoreV1(), diff --git a/selectel/provider_test.go b/selectel/provider_test.go index f01b9d2a..63ba8d82 100644 --- a/selectel/provider_test.go +++ b/selectel/provider_test.go @@ -45,6 +45,13 @@ func testAccSelectelPreCheck(t *testing.T) { } } +func testAccSelectelPreCheckWithProjectID(t *testing.T) { + testAccSelectelPreCheck(t) + if v := os.Getenv("SEL_PROJECT_ID"); v == "" { + t.Fatal("SEL_PROJECT_ID must be set for acceptance tests") + } +} + func testAccCheckSelectelImportEnv(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/selectel/resource_selectel_domains_rrset_v2.go b/selectel/resource_selectel_domains_rrset_v2.go new file mode 100644 index 00000000..f4f442e5 --- /dev/null +++ b/selectel/resource_selectel_domains_rrset_v2.go @@ -0,0 +1,234 @@ +package selectel + +import ( + "context" + "errors" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + domainsV2 "github.com/selectel/domains-go/pkg/v2" +) + +var ErrRRSetNotFound = errors.New("rrset not found") + +func resourceDomainsRRSetV2() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDomainsRRSetV2Create, + ReadContext: resourceDomainsRRSetV2Read, + UpdateContext: resourceDomainsRRSetV2Update, + DeleteContext: resourceDomainsRRSetV2Delete, + Importer: &schema.ResourceImporter{ + StateContext: resourceDomainsRRSetV2ImportState, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + }, + "managed_by": { + Type: schema.TypeString, + Computed: true, + }, + "ttl": { + Type: schema.TypeInt, + Required: true, + }, + "records": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Required: true, + }, + "disabled": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceDomainsRRSetV2Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + zoneID := d.Get("zone_id").(string) + + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(err) + } + + recordType := domainsV2.RecordType(d.Get("type").(string)) + recordsSet := d.Get("records").(*schema.Set) + records := generateRecordsFromSet(recordsSet) + createOpts := domainsV2.RRSet{ + Name: d.Get("name").(string), + Type: recordType, + TTL: d.Get("ttl").(int), + ZoneID: zoneID, + Records: records, + } + + if comment := d.Get("comment"); comment != nil { + createOpts.Comment = comment.(string) + } + if managedBy := d.Get("managed_by"); managedBy != nil { + createOpts.ManagedBy = managedBy.(string) + } + + rrset, err := client.CreateRRSet(ctx, zoneID, &createOpts) + if err != nil { + return diag.FromErr(errCreatingObject(objectRRSet, err)) + } + + err = setRRSetToResourceData(d, rrset) + if err != nil { + return diag.FromErr(errCreatingObject(objectRRSet, err)) + } + + return nil +} + +func resourceDomainsRRSetV2Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(err) + } + + zoneID := d.Get("zone_id").(string) + zoneIDWithRRSetID := fmt.Sprintf("zone_id: %s, rrset_id: %s", zoneID, d.Id()) + + log.Print(msgGet(objectRRSet, zoneIDWithRRSetID)) + + rrset, err := client.GetRRSet(ctx, zoneID, d.Id()) + if err != nil { + d.SetId("") + return diag.FromErr(errGettingObject(objectRRSet, zoneIDWithRRSetID, err)) + } + + err = setRRSetToResourceData(d, rrset) + if err != nil { + return diag.FromErr(errGettingObject(objectRRSet, zoneIDWithRRSetID, err)) + } + + return nil +} + +func resourceDomainsRRSetV2ImportState(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if config.ProjectID == "" { + return nil, errors.New("SEL_PROJECT_ID must be set for the resource import") + } + d.Set("project_id", config.ProjectID) + + client, err := getDomainsV2Client(d, meta) + if err != nil { + return nil, err + } + // concat zone_name,rrset_name,rrset_type with symbol "/" instead of rrset id for importing rrset. + // example: terraform import domains_rrset_v2. // + parts := strings.Split(d.Id(), "/") + if len(parts) != 3 { + return nil, errors.New("id must include three parts: zone_name/rrset_name/rrset_type") + } + + zoneName := parts[0] + rrsetName := parts[1] + rrsetType := parts[2] + + log.Print(msgImport(objectRRSet, fmt.Sprintf("%s/%s/%s", zoneName, rrsetName, rrsetType))) + + zone, err := getZoneByName(ctx, client, zoneName) + if err != nil { + return nil, err + } + + rrset, err := getRRSetByNameAndType(ctx, client, zone.ID, rrsetName, rrsetType) + if err != nil { + return nil, err + } + + err = setRRSetToResourceData(d, rrset) + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func resourceDomainsRRSetV2Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + zoneID := d.Get("zone_id").(string) + + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(errUpdatingObject(objectRRSet, d.Id(), err)) + } + + if d.HasChanges("ttl", "comment", "records") { + recordsSet := d.Get("records").(*schema.Set) + records := generateRecordsFromSet(recordsSet) + + updateOpts := domainsV2.RRSet{ + Name: d.Get("name").(string), + Type: domainsV2.RecordType(d.Get("type").(string)), + TTL: d.Get("ttl").(int), + ZoneID: zoneID, + ManagedBy: d.Get("managed_by").(string), + Records: records, + } + if comment, ok := d.GetOk("comment"); ok { + updateOpts.Comment = comment.(string) + } + err = client.UpdateRRSet(ctx, zoneID, d.Id(), &updateOpts) + if err != nil { + return diag.FromErr(errUpdatingObject(objectRRSet, d.Id(), err)) + } + } + + return resourceDomainsRRSetV2Read(ctx, d, meta) +} + +func resourceDomainsRRSetV2Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + zoneID := d.Get("zone_id").(string) + + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(errDeletingObject(objectRRSet, d.Id(), err)) + } + + log.Print(msgDelete(objectRRSet, fmt.Sprintf("zone_id: %s, rrset_id: %s", zoneID, d.Id()))) + + err = client.DeleteRRSet(ctx, zoneID, d.Id()) + if err != nil { + return diag.FromErr(errDeletingObject(objectRRSet, d.Id(), err)) + } + + return nil +} diff --git a/selectel/resource_selectel_domains_rrset_v2_test.go b/selectel/resource_selectel_domains_rrset_v2_test.go new file mode 100644 index 00000000..282ce2e0 --- /dev/null +++ b/selectel/resource_selectel_domains_rrset_v2_test.go @@ -0,0 +1,100 @@ +package selectel + +import ( + "context" + "errors" + "fmt" + "log" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + domainsV2 "github.com/selectel/domains-go/pkg/v2" +) + +const resourceRRSetName = "rrset_tf_acc_test_1" + +func TestAccDomainsRRSetV2Basic(t *testing.T) { + projectName := acctest.RandomWithPrefix("tf-acc") + testZoneName := fmt.Sprintf("%s.ru.", acctest.RandomWithPrefix("tf-acc")) + testRRSetName := fmt.Sprintf("%[1]s.%[2]s", acctest.RandomWithPrefix("tf-acc"), testZoneName) + testRRSetType := domainsV2.TXT + testRRSetTTL := 60 + testRRSetContent := fmt.Sprintf("\"%[1]s\"", acctest.RandString(16)) + resourceZoneName := "zone_tf_acc_test_1" + resourceRRSetName := "rrset_tf_acc_test_1" + dataSourceRRSetName := fmt.Sprintf("selectel_domains_rrset_v2.%[1]s", resourceRRSetName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckDomainsV2ZoneDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDomainsRRSetV2WithZoneBasic(projectName, resourceRRSetName, testRRSetName, string(testRRSetType), testRRSetContent, testRRSetTTL, resourceZoneName, testZoneName), + Check: resource.ComposeTestCheckFunc( + testAccDomainsRRSetV2ID(dataSourceRRSetName), + resource.TestCheckResourceAttr(dataSourceRRSetName, "name", testRRSetName), + resource.TestCheckResourceAttr(dataSourceRRSetName, "type", string(testRRSetType)), + resource.TestCheckResourceAttrSet(dataSourceRRSetName, "zone_id"), + ), + }, + }, + }) +} + +func testAccDomainsRRSetV2WithZoneBasic(projectName, resourceRRSetName, rrsetName, rrsetType, rrsetContent string, ttl int, resourceZoneName, zoneName string) string { + return fmt.Sprintf(` + %[7]s + + resource "selectel_domains_rrset_v2" %[1]q { + name = %[2]q + type = %[3]q + ttl = %[4]d + zone_id = selectel_domains_zone_v2.%[6]s.id + project_id = "${selectel_vpc_project_v2.project_tf_acc_test_1.id}" + records { + content = %[5]q + disabled = false + } + }`, resourceRRSetName, rrsetName, rrsetType, ttl, rrsetContent, resourceZoneName, testAccDomainsZoneV2Basic(projectName, resourceZoneName, zoneName)) +} + +func testAccCheckDomainsV2RRSetDestroy(s *terraform.State) error { + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + log.Printf("RT: %s", rs.Type) + if rs.Type != "selectel_domains_rrset_v2" { + continue + } + + zoneID := rs.Primary.Attributes["zone_id"] + rrsetID := rs.Primary.ID + client, err := getDomainsV2ClientTest(rs, testAccProvider) + if err != nil { + return err + } + _, err = client.GetRRSet(ctx, zoneID, rrsetID) + if err == nil { + return errors.New("rrset still exists") + } + } + + return nil +} + +func testAccDomainsRRSetV2ID(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("can't find rrset: %s", name) + } + + if rs.Primary.ID == "" { + return errors.New("rrset data source ID not set") + } + + return nil + } +} diff --git a/selectel/resource_selectel_domains_zone_v2.go b/selectel/resource_selectel_domains_zone_v2.go new file mode 100644 index 00000000..b0a9590d --- /dev/null +++ b/selectel/resource_selectel_domains_zone_v2.go @@ -0,0 +1,214 @@ +package selectel + +import ( + "context" + "errors" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + domainsV2 "github.com/selectel/domains-go/pkg/v2" +) + +var ErrZoneNotFound = errors.New("zone not found") + +func resourceDomainsZoneV2() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDomainsZoneV2Create, + ReadContext: resourceDomainsZoneV2Read, + DeleteContext: resourceDomainsZoneV2Delete, + UpdateContext: resourceDomainsZoneV2Update, + Importer: &schema.ResourceImporter{ + StateContext: resourceDomainsZoneV2ImportState, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + }, + "delegation_checked_at": { + Type: schema.TypeString, + Computed: true, + }, + "last_check_status": { + Type: schema.TypeBool, + Computed: true, + }, + "last_delegated_at": { + Type: schema.TypeString, + Computed: true, + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + } +} + +func resourceDomainsZoneV2Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(err) + } + + zoneName := d.Get("name").(string) + createOpts := domainsV2.Zone{ + Name: zoneName, + } + + log.Println(msgCreate(objectZone, zoneName)) + + zone, err := client.CreateZone(ctx, &createOpts) + if err != nil { + return diag.FromErr(errCreatingObject(objectZone, err)) + } + // Update comment after creating + // because set comment in creating request not supporting + if v, ok := d.GetOk("comment"); ok { + comment := v.(string) + err = client.UpdateZoneComment(ctx, zone.ID, comment) + if err != nil { + return diag.FromErr(errUpdatingObject(objectZone, zone.ID, err)) + } + } + // Update disabled after creating + // because set disabled in creating request not supporting + if v, ok := d.GetOk("disabled"); ok { + disabled := v.(bool) + err = client.UpdateZoneState(ctx, zone.ID, disabled) + if err != nil { + return diag.FromErr(errUpdatingObject(objectZone, zone.ID, err)) + } + } + + err = setZoneToResourceData(d, zone) + if err != nil { + return diag.FromErr(errCreatingObject(objectZone, err)) + } + + return nil +} + +func resourceDomainsZoneV2Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(err) + } + + zoneName := d.Get("name").(string) + + log.Println(msgGet(objectZone, zoneName)) + zone, err := getZoneByName(ctx, client, zoneName) + if err != nil { + return diag.FromErr(errGettingObject(objectZone, zoneName, err)) + } + + err = setZoneToResourceData(d, zone) + if err != nil { + return diag.FromErr(errGettingObject(objectZone, zoneName, err)) + } + + return nil +} + +func resourceDomainsZoneV2ImportState(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if config.ProjectID == "" { + return nil, errors.New("SEL_PROJECT_ID must be set for the resource import") + } + d.Set("project_id", config.ProjectID) + + client, err := getDomainsV2Client(d, meta) + if err != nil { + return nil, err + } + + // use zone name instead of zone id for importing zone. + // example: terraform import domains_zone_v2. + zoneName := d.Id() + + log.Println(msgImport(objectZone, zoneName)) + + zone, err := getZoneByName(ctx, client, zoneName) + if err != nil { + return nil, err + } + + err = setZoneToResourceData(d, zone) + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func resourceDomainsZoneV2Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(errUpdatingObject(objectZone, d.Id(), err)) + } + + if d.HasChange("comment") { + comment := "" + if v, ok := d.GetOk("comment"); ok { + comment = v.(string) + } + log.Println(msgUpdate(objectZone, d.Id(), comment)) + + err = client.UpdateZoneComment(ctx, d.Id(), comment) + if err != nil { + return diag.FromErr(errUpdatingObject(objectZone, d.Id(), err)) + } + } + + if d.HasChange("disabled") { + disabled := false + if v, ok := d.GetOk("disabled"); ok { + disabled = v.(bool) + } + log.Println(msgUpdate(objectZone, d.Id(), disabled)) + + err = client.UpdateZoneState(ctx, d.Id(), disabled) + if err != nil { + return diag.FromErr(errUpdatingObject(objectZone, d.Id(), err)) + } + } + + return nil +} + +func resourceDomainsZoneV2Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getDomainsV2Client(d, meta) + if err != nil { + return diag.FromErr(err) + } + + log.Println(msgDelete(objectZone, d.Id())) + + err = client.DeleteZone(ctx, d.Id()) + if err != nil { + return diag.FromErr(errDeletingObject(objectZone, d.Id(), err)) + } + + return nil +} diff --git a/selectel/resource_selectel_domains_zone_v2_test.go b/selectel/resource_selectel_domains_zone_v2_test.go new file mode 100644 index 00000000..a98722a3 --- /dev/null +++ b/selectel/resource_selectel_domains_zone_v2_test.go @@ -0,0 +1,92 @@ +package selectel + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const resourceZoneName = "zone_tf_acc_test_1" + +func TestAccDomainsZoneV2Basic(t *testing.T) { + projectName := acctest.RandomWithPrefix("tf-acc") + testZoneName := fmt.Sprintf("%s.xyz.", acctest.RandomWithPrefix("tf-acc")) + resourceZoneName := "zone_tf_acc_test_1" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckDomainsV2ZoneDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDomainsZoneV2Basic(projectName, resourceZoneName, testZoneName), + Check: resource.ComposeTestCheckFunc( + testAccDomainsZoneV2Exists(fmt.Sprintf("selectel_domains_zone_v2.%[1]s", resourceZoneName)), + resource.TestCheckResourceAttr(fmt.Sprintf("selectel_domains_zone_v2.%[1]s", resourceZoneName), "name", testZoneName), + ), + }, + }, + }) +} + +func testAccDomainsZoneV2Basic(projectName, resourceName, zoneName string) string { + return fmt.Sprintf(` + resource "selectel_vpc_project_v2" "project_tf_acc_test_1" { + name = %[1]q + } + resource "selectel_domains_zone_v2" %[2]q { + name = %[3]q + project_id = selectel_vpc_project_v2.project_tf_acc_test_1.id + }`, projectName, resourceName, zoneName) +} + +func testAccCheckDomainsV2ZoneDestroy(s *terraform.State) error { + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "selectel_domains_zone_v2" { + continue + } + + zoneID := rs.Primary.ID + client, err := getDomainsV2ClientTest(rs, testAccProvider) + if err != nil { + return err + } + _, err = client.GetZone(ctx, zoneID, nil) + if err == nil { + return errors.New("domain still exists") + } + } + + return nil +} + +func testAccDomainsZoneV2Exists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("can't find zone: %s", name) + } + + zoneID := rs.Primary.ID + if zoneID == "" { + return errors.New("zone ID not set in tf state") + } + client, err := getDomainsV2ClientTest(rs, testAccProvider) + if err != nil { + return err + } + ctx := context.Background() + _, err = client.GetZone(ctx, zoneID, nil) + if err != nil { + return errors.New("zone in api not found") + } + + return nil + } +} diff --git a/website/docs/d/domains_domain_v1.html.markdown b/website/docs/d/domains_domain_v1.html.markdown index edcba2bf..1819b9aa 100644 --- a/website/docs/d/domains_domain_v1.html.markdown +++ b/website/docs/d/domains_domain_v1.html.markdown @@ -3,12 +3,14 @@ layout: "selectel" page_title: "Selectel: selectel_domains_domain_v1" sidebar_current: "docs-selectel-datasource-domains-domain-v1" description: |- - Provides an ID of a domain in Selectel DNS Hosting. + Provides an ID of a domain in Selectel DNS Hosting (legacy). --- # selectel\_domains\_domain_v1 -Provides an ID of a domain in DNS Hosting. For more information about domains in DNS Hosting, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/domains/). +**WARNING**: This data source is applicable to DNS Hosting (legacy). We do not support and develop DNS Hosting (legacy), but domains and records created in DNS Hosting (legacy) continue to work until further notice. We recommend to transfer your data to DNS Hosting (actual). For more infomation about DNS Hosting (actual), see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/about-dns/). + +Provides an ID of a domain in DNS Hosting (legacy). ## Example Usage diff --git a/website/docs/r/domains_domain_v1.html.markdown b/website/docs/r/domains_domain_v1.html.markdown index a9498fad..0f680ccd 100644 --- a/website/docs/r/domains_domain_v1.html.markdown +++ b/website/docs/r/domains_domain_v1.html.markdown @@ -8,7 +8,10 @@ description: |- # selectel\_domains\_domain\_v1 -Creates and manages a domain in DNS Hosting using public API v1. For more information about domains, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/domains/). +**WARNING**: This resource is applicable to DNS Hosting (legacy). We do not support and develop DNS Hosting (legacy), but domains and records created in DNS Hosting (legacy) continue to work until further notice. We recommend to transfer your data to DNS Hosting (actual). For more infomation about DNS Hosting (actual), see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/about-dns/). +To create zones for your domain records in DNS Hosting (actual) use the [selectel_domains_zone_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/selectel_domains_zone_v2) resource. + +Creates and manages a domain in DNS Hosting (legacy) using public API v1. For more information about domains, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/zones/). ## Example usage diff --git a/website/docs/r/domains_record_v1.html.markdown b/website/docs/r/domains_record_v1.html.markdown index 822bf577..63eebb0b 100644 --- a/website/docs/r/domains_record_v1.html.markdown +++ b/website/docs/r/domains_record_v1.html.markdown @@ -8,7 +8,10 @@ description: |- # selectel\_domains\_record\_v1 -Creates and manages a record in DNS Hosting using public API v1. For more information about records, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/records/add-record/). +**WARNING**: This resource is applicable to DNS Hosting (legacy). We do not support and develop DNS Hosting (legacy), but domains and records created in DNS Hosting (legacy) continue to work until further notice. We recommend to transfer your data to DNS Hosting (actual). For more infomation about DNS Hosting (actual), see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/about-dns/). +To create records in DNS Hosting (actual) use the [selectel_domains_rrset_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/selectel_domains_rrset_v2) resource. + +Creates and manages a record in DNS Hosting (legacy) using public API v1. For more information about records, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/records/). ## Example usage @@ -17,7 +20,7 @@ Creates and manages a record in DNS Hosting using public API v1. For more inform ```hcl resource "selectel_domains_record_v1" "a_record_1" { domain_id = selectel_domains_domain_v1.domain_1.id - name = "a.example.com" + name = "example.com" type = "A" content = "127.0.0.1" ttl = 60 @@ -29,7 +32,7 @@ resource "selectel_domains_record_v1" "a_record_1" { ```hcl resource "selectel_domains_record_v1" "aaaa_record_1" { domain_id = selectel_domains_domain_v1.domain_1.id - name = "aaaa.example.com" + name = "example.com" type = "AAAA" content = "2400:cb00:2049:1::a29f:1804" ttl = 60 @@ -41,7 +44,7 @@ resource "selectel_domains_record_v1" "aaaa_record_1" { ```hcl resource "selectel_domains_record_v1" "txt_record_1" { domain_id = selectel_domains_domain_v1.domain_1.id - name = "txt.example.com" + name = "example.com" type = "TXT" content = "hello, world!" ttl = 60 @@ -53,7 +56,7 @@ resource "selectel_domains_record_v1" "txt_record_1" { ```hcl resource "selectel_domains_record_v1" "cname_record_1" { domain_id = selectel_domains_domain_v1.domain_1.id - name = "cname.example.com" + name = "example.com" type = "CNAME" content = "origin.com" ttl = 60 @@ -77,7 +80,7 @@ resource "selectel_domains_record_v1" "ns_record_1" { ```hcl resource "selectel_domains_record_v1" "mx_record_1" { domain_id = selectel_domains_domain_v1.domain_1.id - name = "mx.example.com" + name = "example.com" type = "MX" content = "mail.example.org" ttl = 60 @@ -90,12 +93,12 @@ resource "selectel_domains_record_v1" "mx_record_1" { ```hcl resource "selectel_domains_record_v1" "srv_record_1" { domain_id = selectel_domains_domain_v1.domain_1.id - name = "srv.example.com" + name = "example.com" type = "SRV" ttl = 120 priority = 10 weight = 20 - target = "backupbox.example.com" + target = "example.com" port = 100 } ``` diff --git a/website/docs/r/domains_rrset_v2.html.markdown b/website/docs/r/domains_rrset_v2.html.markdown new file mode 100644 index 00000000..ed2d9732 --- /dev/null +++ b/website/docs/r/domains_rrset_v2.html.markdown @@ -0,0 +1,224 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_domains_rrset_v2" +sidebar_current: "docs-selectel-resource-domains-rrset-v2" +description: |- + Creates and manages an RRSet in Selectel DNS Hosting (actual) using public API v2. +--- + +# selectel\_domains\_rrset\_v2 + +Creates and manages an RRSet in DNS Hosting (actual) using public API v2. For more information about RRSets, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/records/). + +## Example usage + +### A RRSet + +```hcl +resource "selectel_domains_rrset_v2" "a_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "A" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "127.0.0.1" + # The content value is "" + } +} +``` + +### AAAA RRSet + +```hcl +resource "selectel_domains_rrset_v2" "aaaa_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "AAAA" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "2400:cb00:2049:1::a29f:1804" + # The content value is "" + } +} +``` + +### TXT RRSet + +```hcl +resource "selectel_domains_rrset_v2" "txt_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "TXT" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "\"hello, world!\"" + # The content value is "" + } +} +``` + +### CNAME RRSet + +```hcl +resource "selectel_domains_rrset_v2" "cname_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "CNAME" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "origin.com." + # The content value is "" + } +} +``` + +### MX RRSet + +```hcl +resource "selectel_domains_rrset_v2" "mx_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "MX" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "10 mail.example.org." + # The content value is " " + } +} +``` + +### SRV RRSet + +```hcl +resource "selectel_domains_rrset_v2" "srv_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "_sip._tcp.example.com." + type = "SRV" + ttl = 120 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "10 20 30 example.org." + # The content value is " " + } +} +``` + +### SSHFP RRSet + +```hcl +resource "selectel_domains_rrset_v2" "sshfp_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "SSHFP" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49" + # The content value is " " + } +} +``` + +### ALIAS RRSet + +```hcl +resource "selectel_domains_rrset_v2" "alias_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "ALIAS" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "origin.com." + # The content value is "" + } +} +``` + +### CAA RRSet + +```hcl +resource "selectel_domains_rrset_v2" "caa_rrset_1" { + zone_id = selectel_domains_zone_v2.zone_1.id + name = "example.com." + type = "CAA" + ttl = 60 + project_id = selectel_vpc_project_v2.project_1.id + records { + content = "128 issue \"letsencrypt.com.\"" + # The content value is " " + } +} +``` + +## Argument Reference + +* `zone_id` - (Required) Unique identifier of the zone. Changing this creates a new RRSet. Retrieved from the [selectel_domains_zone_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/domains_zone_v2) resource. + +* `name` - (Required) RRSet name. Changing this creates a new RRSet. The value must be the same as the zone name. If `type` is `SRV`, the name must also include service and protocol, see the [example usage for SRV RRSet](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/domains_rrset_v2#srv-rrset). + +* `type` - (Required) RRSet type. Changing this creates a new RRSet. Available types are `A`, `AAAA`, `TXT`, `CNAME`, `MX`, `SRV`, `SSHFP`, `ALIAS`, `CAA`. + +* `ttl` - (Required) RRSet time-to-live in seconds. The available range is from 60 to 604800. + +* `records` - (Required) List of records in the RRSet. + + * `content` - (Required) Record value. The value depends on the RRSet type. + - `` — IPv4-address. Applicable only to A RRSets. + - `` — IPv6-address. Applicable only to AAAA RRSets. + - `` — Any text wrapped in `\"`. Applicable only to TXT RRSets. + - `` — Canonical name of the host providing the service with a dot at the end. Applicable only to CNAME, ALIAS, and SRV RRSets. + - `` — Canonical name of the NS server. Applicable only to NS RRSets. + - `` — Priority of the records preferences. Applicable only to MX and SRV RRSets. Lower value means more preferred. + - `` — Name of the mailserver with a dot at the end. Applicable only to MX RRSets. + - `` — Weight for the records with the same priority. Higher value means more preferred. Applicable only to SRV RRSets. + - `` — TCP or UDP port of the host of the service. Applicable only to SRV RRSets. + - `` — Algorithm of the public key. Applicable only to SSHFP RRSets. Available values are `1` for RSA, `2` for DSA, `3` for ECDSA, `4` for Ed25519. + - `` — Algorithm used to hash the public key. Applicable only to SSHFP RRSets. Available values are `1` for SHA-1, `2` for SHA-256. + - `` — Hexadecimal representation of the hash result, as text. Applicable only to SSHFP RRSets. + - `` — Critical value that has a specific meaning per RFC. Applicable only to CAA RRSets. The available range is from 0 to 128. + - `` — Identifier of the property represented by the record. Applicable only to CAA RRSets. Available values are `issue`, `issuewild`, `iodef`, `auth`, `path`, `policy`. + - `` — Value associated with the tag wrapped in `\"`. Applicable only to CAA RRSets. + + * `disabled` - (Optional) Enables or disables the record. Boolean flag, the default value is false. + +* `project_id` - (Required) Unique identifier of the associated Cloud Platform project. Changing this creates a new RRSet. Retrieved from the [selectel_vpc_project_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/vpc_project_v2) resource. Learn more about [Cloud Platform projects](https://docs.selectel.ru/cloud/servers/about/projects/). + +* `comment` - (Optional) Comment to add to the RRSet. + +## Attributes Reference + +* `managed_by` - RRSet owner. + +## Import + +You can import an RRSet: + +```shell +export OS_DOMAIN_NAME= +export OS_USERNAME= +export OS_PASSWORD= +export SEL_PROJECT_ID= +terraform import selectel_domains_rrset_v2.rrset_1 // +``` + +where: + +* `` — Selectel account ID. The account ID is in the top right corner of the [Control panel](https://my.selectel.ru/). Learn more about [Registration](https://docs.selectel.ru/control-panel-actions/account/registration/). + +* `` — Name of the service user. To get the name, in the top right corner of the [Control panel](https://my.selectel.ru/profile/users_management/users?type=service), go to the account menu ⟶ **Profile and Settings** ⟶ **User management** ⟶ the **Service users** tab ⟶ copy the name of the required user. Learn more about [Service users](https://docs.selectel.ru/control-panel-actions/users-and-roles/user-types-and-roles/). + +* `` — Password of the service user. + +* `` — Unique identifier of the associated Cloud Platform project. To get the project ID, in the [Control panel](https://my.selectel.ru/vpc/), go to Cloud Platform ⟶ project name ⟶ copy the ID of the required project. Learn more about [Cloud Platform projects](https://docs.selectel.ru/cloud/servers/about/projects/). + +* `` — Zone name, for example, `example.com.`. To get the name, in the [Control panel](https://my.selectel.ru/dns/), go to **DNS**. The zone name is in the **Zone** column. + +* `` — RRSet name, for example, `example.com.`. To get the name, in the [Control panel](https://my.selectel.ru/dns/), go to **DNS** → the zone page. The RRSet name is in the **Group name** column. + +* `` — RRSet type. To get the type, in the [Control panel](https://my.selectel.ru/dns/), go to **DNS** → the zone page. The RRSet type is in the **Type** column. diff --git a/website/docs/r/domains_zone_v2.html.markdown b/website/docs/r/domains_zone_v2.html.markdown new file mode 100644 index 00000000..47c300cd --- /dev/null +++ b/website/docs/r/domains_zone_v2.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_domains_zone_v2" +sidebar_current: "docs-selectel-resource-domains-zone-v2" +description: |- + Creates and manages a zone in Selectel DNS Hosting (actual) using public API v2. +--- + +# selectel\_domains\_zone\_v2 + +Creates and manages a zone in DNS Hosting (actual) using public API v2. For more information about zones, see the [official Selectel documentation](https://docs.selectel.ru/networks-services/dns/zones/). + +## Example usage + +```hcl +resource "selectel_domains_zone_v2" "zone_1" { + name = "example.com." + project_id = selectel_vpc_project_v2.project_1.id +} +``` + +## Argument Reference + +* `name` - (Required) Zone name. Changing this creates a new zone. + +* `project_id` - (Required) Unique identifier of the associated Cloud Platform project. Retrieved from the [selectel_vpc_project_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/vpc_project_v2) resource. Learn more about [Cloud Platform projects](https://docs.selectel.ru/cloud/servers/about/projects/). + +* `comment` - (Optional) Comment to add to the zone. + +* `disabled` - (Optional) Enables or disables the zone. Boolean flag, the default value is false. + +## Attributes Reference + +* `created_at` - Time when the zone was created in the RFC 3339 timestamp format. + +* `updated_at` - Time when the zone was updated in the RFC 3339 timestamp format. + +* `delegation_checked_at` - Time when DNS Hosting checked if the zone was delegated to Selectel NS servers in the RFC 3339 timestamp format. + +* `last_check_status` - Zone status retrieved during the last delegation check. + +* `last_delegated_at` - Equals to the `delegation_check_at` argument value when the `last_check_status` is `true`. + +## Import + +You can import a zone: + +```shell +export OS_DOMAIN_NAME= +export OS_USERNAME= +export OS_PASSWORD= +export SEL_PROJECT_ID= +terraform import selectel_domains_zone_v2.zone_1 +``` + +where: + +* `` — Selectel account ID. The account ID is in the top right corner of the [Control panel](https://my.selectel.ru/). Learn more about [Registration](https://docs.selectel.ru/control-panel-actions/account/registration/). + +* `` — Name of the service user. To get the name, in the top right corner of the [Control panel](https://my.selectel.ru/profile/users_management/users?type=service), go to the account menu ⟶ **Profile and Settings** ⟶ **User management** ⟶ the **Service users** tab ⟶ copy the name of the required user. Learn more about [Service users](https://docs.selectel.ru/control-panel-actions/users-and-roles/user-types-and-roles/). + +* `` — Password of the service user. + +* `` — Unique identifier of the associated Cloud Platform project. To get the project ID, in the [Control panel](https://my.selectel.ru/vpc/), go to Cloud Platform ⟶ project name ⟶ copy the ID of the required project. Learn more about [Cloud Platform projects](https://docs.selectel.ru/cloud/servers/about/projects/). + +* `` — Zone name, for example, `example.com.`. To get the name, in the [Control panel](https://my.selectel.ru/dns/), go to **DNS**. The zone name is in the **Zone** column. diff --git a/website/selectel.erb b/website/selectel.erb index 702c0411..017f188a 100644 --- a/website/selectel.erb +++ b/website/selectel.erb @@ -94,6 +94,12 @@ > selectel_domains_record_v1 + > + selectel_domains_zone_v2 + + > + selectel_domains_rrset_v2 +