From a0bac86ad5897491341442dc9fd05a33949cd1b1 Mon Sep 17 00:00:00 2001 From: Maksym Nazarenko Date: Fri, 21 Jun 2024 16:58:54 +0300 Subject: [PATCH 1/2] dns cache static: add regexp attribute support --- client/client_crud.go | 9 +++ client/dns.go | 17 +++--- client/dns_test.go | 61 +++++++++++++------ client/pool_test.go | 22 +++---- client/resource_wrappers.go | 32 ++++++++++ docs/resources/dns_record.md | 16 ++++- .../resources/mikrotik_dns_record/import.sh | 7 ++- .../resources/mikrotik_dns_record/resource.tf | 6 ++ mikrotik/resource_dns_record.go | 28 +++++++-- mikrotik/resource_dns_record_test.go | 45 ++++++++++++-- 10 files changed, 190 insertions(+), 53 deletions(-) create mode 100644 client/resource_wrappers.go diff --git a/client/client_crud.go b/client/client_crud.go index c8657adb..70b93779 100644 --- a/client/client_crud.go +++ b/client/client_crud.go @@ -66,6 +66,11 @@ type ( ErrorHandler interface { HandleError(error) error } + + // ResourceInstanceCreator interface defines methods to create new instance of a Resource. + ResourceInstanceCreator interface { + Create() Resource + } ) // Add creates new resource on remote system @@ -221,5 +226,9 @@ func (client Mikrotik) findByField(d Resource, field, value string) (Resource, e } func (client Mikrotik) newTargetStruct(d interface{}) reflect.Value { + if c, ok := d.(ResourceInstanceCreator); ok { + return reflect.New(reflect.Indirect(reflect.ValueOf(c.Create())).Type()) + } + return reflect.New(reflect.Indirect(reflect.ValueOf(d)).Type()) } diff --git a/client/dns.go b/client/dns.go index ed7a988e..13499033 100644 --- a/client/dns.go +++ b/client/dns.go @@ -8,8 +8,9 @@ import ( type DnsRecord struct { Id string `mikrotik:".id" codegen:"id,mikrotikID"` Name string `mikrotik:"name" codegen:"name,terraformID,required"` - Ttl types.MikrotikDuration `mikrotik:"ttl" codegen:"ttl"` Address string `mikrotik:"address" codegen:"address,required"` + Regexp string `mikrotik:"regexp" codegen:"regexp"` + Ttl types.MikrotikDuration `mikrotik:"ttl" codegen:"ttl"` Comment string `mikrotik:"comment" codegen:"comment"` } @@ -39,14 +40,6 @@ func (d *DnsRecord) AfterAddHook(r *routeros.Reply) { d.Id = r.Done.Map["ret"] } -func (d *DnsRecord) FindField() string { - return "name" -} - -func (d *DnsRecord) FindFieldValue() string { - return d.Name -} - func (d *DnsRecord) DeleteField() string { return "numbers" } @@ -65,7 +58,11 @@ func (client Mikrotik) AddDnsRecord(d *DnsRecord) (*DnsRecord, error) { } func (client Mikrotik) FindDnsRecord(name string) (*DnsRecord, error) { - res, err := client.Find(&DnsRecord{Name: name}) + res, err := client.Find(&FindByFieldWrapper{ + Resource: &DnsRecord{Name: name}, + field: "name", + fieldValueFunc: func() string { return name }, + }) if err != nil { return nil, err } diff --git a/client/dns_test.go b/client/dns_test.go index eb5b2a46..cee89344 100644 --- a/client/dns_test.go +++ b/client/dns_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -17,7 +18,7 @@ func TestFindDnsRecord_onNonExistantDnsRecord(t *testing.T) { "Expecting to receive NotFound error for dns record %q", name) } -func TestAddFindDeleteDnsRecord(t *testing.T) { +func TestDnsRecord_basic(t *testing.T) { c := NewClient(GetConfigFromEnv()) recordName := "new_record" @@ -34,29 +35,55 @@ func TestAddFindDeleteDnsRecord(t *testing.T) { return } - findRecord := &DnsRecord{} - findRecord.Name = recordName - found, err := c.Find(findRecord) - if err != nil { - t.Errorf("expected no error, got %v", err) - return - } + found, err := c.Find(&DnsRecord{Id: created.ID()}) + require.NoError(t, err) - if _, ok := found.(Resource); !ok { - t.Error("expected found resource to implement Resource interface, but it doesn't") - return - } if !reflect.DeepEqual(created, found) { t.Error("expected created and found resources to be equal, but they don't") } - err = c.Delete(found.(Resource)) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - _, err = c.Find(findRecord) + created.(*DnsRecord).Comment = "updated comment" + _, err = c.Update(created) + require.NoError(t, err) + found, err = c.Find(&DnsRecord{Id: created.ID()}) + require.NoError(t, err) + assert.Equal(t, created, found) + + err = c.Delete(found) + assert.NoError(t, err) + + _, err = c.Find(&DnsRecord{Id: created.ID()}) require.Error(t, err) require.True(t, IsNotFoundError(err), "expected to get NotFound error") } + +func TestDns_Regexp(t *testing.T) { + c := NewClient(GetConfigFromEnv()) + + recordName := "new_record" + record := &DnsRecord{ + Name: recordName, + Regexp: ".*\\.domain\\.com", + Address: "10.10.10.200", + Ttl: 300, + Comment: "new record from test", + } + + _, err := c.Add(record) + require.Error(t, err, "usage of 'name' and 'regexp' at the same type should result in error") + + regexRecord := &DnsRecord{ + Address: "10.10.10.201", + Ttl: 300, + Regexp: ".+\\.domain\\.com", + Comment: "new record from test", + } + regexCreated, err := c.Add(regexRecord) + require.NoError(t, err) + defer func() { + _ = c.Delete(regexCreated) + }() + assert.Equal(t, regexRecord, regexCreated) +} diff --git a/client/pool_test.go b/client/pool_test.go index 9fd31bd8..e750453e 100644 --- a/client/pool_test.go +++ b/client/pool_test.go @@ -7,19 +7,13 @@ import ( "github.com/stretchr/testify/require" ) -var name string = "testacc" -var ranges string = "172.16.0.1-172.16.0.8,172.16.0.10" -var comment string = "terraform-acc-test-pool" -var updatedRanges string = "172.16.0.1-172.16.0.8,172.16.0.16" -var updatedComment string = "terraform acc test pool updated" - func TestAddUpdateAndDeletePool(t *testing.T) { c := NewClient(GetConfigFromEnv()) expectedPool := &Pool{ - Name: name, - Ranges: ranges, - Comment: comment, + Name: "pool-" + RandomString(), + Ranges: "172.16.0.1-172.16.0.8,172.16.0.10", + Comment: "pool comment", } pool, err := c.AddPool(expectedPool) @@ -32,8 +26,8 @@ func TestAddUpdateAndDeletePool(t *testing.T) { t.Errorf("The pool does not match what we expected. actual: %v expected: %v", pool, expectedPool) } - expectedPool.Comment = updatedComment - expectedPool.Ranges = updatedRanges + expectedPool.Comment = "updated comment" + expectedPool.Ranges = "172.16.0.1-172.16.0.8,172.16.0.16" pool, err = c.UpdatePool(expectedPool) if err != nil { @@ -64,9 +58,9 @@ func TestFindPoolByName_forExistingPool(t *testing.T) { c := NewClient(GetConfigFromEnv()) p := &Pool{ - Name: name, - Ranges: ranges, - Comment: comment, + Name: "pool-" + RandomString(), + Ranges: "172.16.0.1-172.16.0.8,172.16.0.10", + Comment: "existing pool", } pool, err := c.AddPool(p) diff --git a/client/resource_wrappers.go b/client/resource_wrappers.go new file mode 100644 index 00000000..7bb21340 --- /dev/null +++ b/client/resource_wrappers.go @@ -0,0 +1,32 @@ +package client + +import "reflect" + +type ( + // FindByFieldWrapper changes the fields used to find the remote resource. + FindByFieldWrapper struct { + Resource + field string + fieldValueFunc func() string + } +) + +var ( + _ Finder = (*FindByFieldWrapper)(nil) + _ Resource = (*FindByFieldWrapper)(nil) +) + +func (fw FindByFieldWrapper) FindField() string { + return fw.field +} + +func (fw FindByFieldWrapper) FindFieldValue() string { + return fw.fieldValueFunc() +} + +// Create satisfies ResourceInstanceCreator interface and returns new object of the wrapped resource. +func (fw FindByFieldWrapper) Create() Resource { + reflectNew := reflect.New(reflect.Indirect(reflect.ValueOf(fw.Resource)).Type()) + + return reflectNew.Interface().(Resource) +} diff --git a/docs/resources/dns_record.md b/docs/resources/dns_record.md index d71ef8fd..80974109 100644 --- a/docs/resources/dns_record.md +++ b/docs/resources/dns_record.md @@ -8,6 +8,12 @@ resource "mikrotik_dns_record" "record" { address = "192.168.88.1" ttl = 300 } + +resource "mikrotik_dns_record" "record_regexp" { + regexp = ".+\\.example\\.domain\\.com" + address = "192.168.88.1" + ttl = 300 +} ``` @@ -16,11 +22,12 @@ resource "mikrotik_dns_record" "record" { ### Required - `address` (String) The A record to be returend from the DNS hostname. -- `name` (String) The name of the DNS hostname to be created. ### Optional - `comment` (String) The comment text associated with the DNS record. +- `name` (String) The name of the DNS hostname to be created. +- `regexp` (String) Regular expression against which domain names should be verified. - `ttl` (Number) The ttl of the DNS record. ### Read-Only @@ -30,5 +37,10 @@ resource "mikrotik_dns_record" "record" { ## Import Import is supported using the following syntax: ```shell -terraform import mikrotik_dns_record.record example.domain.com +# The ID argument (*2) is a MikroTik's internal id. +# It can be obtained via CLI: +# +# [admin@MikroTik] /ip dns static> :put [find where address="192.168.88.1/24"] +# *2 +terraform import mikrotik_dns_record.record "*2" ``` diff --git a/examples/resources/mikrotik_dns_record/import.sh b/examples/resources/mikrotik_dns_record/import.sh index 038a3a43..0d5f1fa5 100644 --- a/examples/resources/mikrotik_dns_record/import.sh +++ b/examples/resources/mikrotik_dns_record/import.sh @@ -1 +1,6 @@ -terraform import mikrotik_dns_record.record example.domain.com +# The ID argument (*2) is a MikroTik's internal id. +# It can be obtained via CLI: +# +# [admin@MikroTik] /ip dns static> :put [find where address="192.168.88.1/24"] +# *2 +terraform import mikrotik_dns_record.record "*2" diff --git a/examples/resources/mikrotik_dns_record/resource.tf b/examples/resources/mikrotik_dns_record/resource.tf index 370470ca..dfa80416 100644 --- a/examples/resources/mikrotik_dns_record/resource.tf +++ b/examples/resources/mikrotik_dns_record/resource.tf @@ -3,3 +3,9 @@ resource "mikrotik_dns_record" "record" { address = "192.168.88.1" ttl = 300 } + +resource "mikrotik_dns_record" "record_regexp" { + regexp = ".+\\.example\\.domain\\.com" + address = "192.168.88.1" + ttl = 300 +} diff --git a/mikrotik/resource_dns_record.go b/mikrotik/resource_dns_record.go index 2cec2310..668b3fa7 100644 --- a/mikrotik/resource_dns_record.go +++ b/mikrotik/resource_dns_record.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ddelnano/terraform-provider-mikrotik/client" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -19,9 +20,10 @@ type dnsRecord struct { // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &dnsRecord{} - _ resource.ResourceWithConfigure = &dnsRecord{} - _ resource.ResourceWithImportState = &dnsRecord{} + _ resource.Resource = &dnsRecord{} + _ resource.ResourceWithConfigure = &dnsRecord{} + _ resource.ResourceWithConfigValidators = &dnsRecord{} + _ resource.ResourceWithImportState = &dnsRecord{} ) // NewDnsRecordResource is a helper function to simplify the provider implementation. @@ -55,9 +57,15 @@ func (s *dnsRecord) Schema(_ context.Context, _ resource.SchemaRequest, resp *re Description: "Unique ID of this resource.", }, "name": schema.StringAttribute{ - Required: true, + Optional: true, + Computed: true, Description: "The name of the DNS hostname to be created.", }, + "regexp": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Regular expression against which domain names should be verified.", + }, "ttl": schema.Int64Attribute{ Optional: true, Computed: true, @@ -76,6 +84,15 @@ func (s *dnsRecord) Schema(_ context.Context, _ resource.SchemaRequest, resp *re } } +func (r *dnsRecord) ConfigValidators(context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("name"), + path.MatchRoot("regexp"), + ), + } +} + // Create creates the resource and sets the initial Terraform state. func (r *dnsRecord) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var terraformModel dnsRecordModel @@ -106,12 +123,13 @@ func (r *dnsRecord) Delete(ctx context.Context, req resource.DeleteRequest, resp func (r *dnsRecord) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Retrieve import ID and save to id attribute - resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } type dnsRecordModel struct { Id tftypes.String `tfsdk:"id"` Name tftypes.String `tfsdk:"name"` + Regexp tftypes.String `tfsdk:"regexp"` Ttl tftypes.Int64 `tfsdk:"ttl"` Address tftypes.String `tfsdk:"address"` Comment tftypes.String `tfsdk:"comment"` diff --git a/mikrotik/resource_dns_record_test.go b/mikrotik/resource_dns_record_test.go index ebac648b..9f61eb98 100644 --- a/mikrotik/resource_dns_record_test.go +++ b/mikrotik/resource_dns_record_test.go @@ -2,6 +2,7 @@ package mikrotik import ( "fmt" + "regexp" "testing" "github.com/ddelnano/terraform-provider-mikrotik/client" @@ -26,6 +27,40 @@ func TestAccMikrotikDnsRecord_create(t *testing.T) { testAccDnsRecordExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "id")), }, + { + Config: ` + resource "mikrotik_dns_record" "bar" { + address = "10.10.200.100" + regexp = ".+\\.domain\\.com" + ttl = "300" + } + `, + ExpectError: regexp.MustCompile("only name or regexp allowed"), + }, + }, + }) +} + +func TestAccMikrotikDnsRecord_createRegexp(t *testing.T) { + resourceName := "mikrotik_dns_record.bar" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + CheckDestroy: testAccCheckMikrotikDnsRecordDestroy, + Steps: []resource.TestStep{ + { + Config: ` + resource "mikrotik_dns_record" "bar" { + address = "10.10.200.100" + regexp = ".+\\.domain\\.com" + ttl = "300" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr("mikrotik_dns_record.bar", "regexp", ".+\\.domain\\.com"), + ), + }, }, }) } @@ -110,6 +145,7 @@ func TestAccMikrotikDnsRecord_updateComment(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccDnsRecordWithComment(dnsName, ipAddr, comment), + Check: resource.ComposeAggregateTestCheckFunc( testAccDnsRecordExists(resourceName), resource.TestCheckResourceAttr(resourceName, "comment", comment), @@ -142,9 +178,9 @@ func TestAccMikrotikDnsRecord_import(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "id")), }, { - ImportState: true, - ResourceName: resourceName, - ImportStateId: dnsName, + ImportState: true, + ResourceName: resourceName, + // ImportStateId: dnsName, ImportStateVerify: true, }, }, @@ -192,12 +228,13 @@ func testAccDnsRecordExists(resourceName string) resource.TestCheckFunc { } if dnsRecord == nil { - return fmt.Errorf("Unable to get the dns record with name: %s", dnsRecord.Name) + return fmt.Errorf("Unable to get the dns record with name: %s", rs.Primary.Attributes["name"]) } if dnsRecord.Name == rs.Primary.Attributes["name"] { return nil } + return nil } } From ad76716945bb2de3f8b1296a11b7e0f6e7b8fcce Mon Sep 17 00:00:00 2001 From: Maksym Nazarenko Date: Wed, 26 Jun 2024 00:31:02 +0300 Subject: [PATCH 2/2] removed commented code --- mikrotik/resource_dns_record_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mikrotik/resource_dns_record_test.go b/mikrotik/resource_dns_record_test.go index 9f61eb98..156046e5 100644 --- a/mikrotik/resource_dns_record_test.go +++ b/mikrotik/resource_dns_record_test.go @@ -178,9 +178,8 @@ func TestAccMikrotikDnsRecord_import(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "id")), }, { - ImportState: true, - ResourceName: resourceName, - // ImportStateId: dnsName, + ImportState: true, + ResourceName: resourceName, ImportStateVerify: true, }, },