diff --git a/.gitignore b/.gitignore index fd3ad8e..f717cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,13 @@ website/node_modules website/vendor +# Terraform files that may be left after running the example +terraform-provider-mreg +.terraform* +terraform.tfstate* +crash.log +terraform-provider-mreg-lockfile + # Test exclusions !command/test-fixtures/**/*.tfstate !command/test-fixtures/**/.terraform/ diff --git a/docs/data-sources/data_source.md b/docs/data-sources/data_source.md deleted file mode 100644 index 89998a5..0000000 --- a/docs/data-sources/data_source.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -page_title: "scaffolding_data_source Data Source - terraform-provider-scaffolding" -subcategory: "" -description: |- - Sample data source in the Terraform provider scaffolding. ---- - -# Data Source `scaffolding_data_source` - -Sample data source in the Terraform provider scaffolding. - -## Example Usage - -```terraform -data "scaffolding_data_source" "example" { - sample_attribute = "foo" -} -``` - -## Schema - -### Required - -- **sample_attribute** (String, Required) Sample attribute. - -### Optional - -- **id** (String, Optional) The ID of this resource. - - diff --git a/docs/index.md b/docs/index.md index ab8ed89..94658c1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,20 +1,22 @@ --- -page_title: "scaffolding Provider" +page_title: "Provider: Mreg" subcategory: "" description: |- - + The Mreg provider provides resources to interact with [Mreg](https://github.com/unioslo/mreg/) through the API. --- -# scaffolding Provider +# Mreg Provider +The Mreg provider provides resources to interact with [Mreg](https://github.com/unioslo/mreg/) through the API. +## Example usage -## Example Usage + provider "mreg" { + serverurl = "https://mreg.example.com/" + token = "1234567890ABCDEF" + } -```terraform -provider "scaffolding" { - # example configuration here -} -``` +### Required configuration options -## Schema +- **serverurl** (String) +- **token** (String, Sensitive) diff --git a/docs/resources/dns_srv.md b/docs/resources/dns_srv.md new file mode 100644 index 0000000..f03f83d --- /dev/null +++ b/docs/resources/dns_srv.md @@ -0,0 +1,30 @@ +--- +page_title: "mreg_dns_srv Resource - terraform-provider-mreg" +subcategory: "" +description: |- + +--- + +# Resource `mreg_dns_srv` + + + + + +## Schema + +### Required + +- **name** (String) +- **port** (Number) +- **priority** (Number) +- **proto** (String) +- **service** (String) +- **target_host** (String) +- **weight** (Number) + +### Optional + +- **id** (String) The ID of this resource. + + diff --git a/docs/resources/hosts.md b/docs/resources/hosts.md new file mode 100644 index 0000000..54a0d86 --- /dev/null +++ b/docs/resources/hosts.md @@ -0,0 +1,44 @@ +--- +page_title: "mreg_hosts Resource - terraform-provider-mreg" +subcategory: "" +description: |- + +--- + +# Resource `mreg_hosts` + + + + + +## Schema + +### Required + +- **contact** (String) +- **host** (Block List, Min: 1) (see [below for nested schema](#nestedblock--host)) +- **network** (String) + +### Optional + +- **comment** (String) +- **id** (String) The ID of this resource. + + +### Nested Schema for `host` + +Required: + +- **name** (String) + +Optional: + +- **manual_ipaddress** (String) + +Read-only: + +- **comment** (String) +- **contact** (String) +- **ipaddress** (String) + + diff --git a/docs/resources/resource.md b/docs/resources/resource.md deleted file mode 100644 index 9d8716d..0000000 --- a/docs/resources/resource.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -page_title: "scaffolding_resource Resource - terraform-provider-scaffolding" -subcategory: "" -description: |- - Sample resource in the Terraform provider scaffolding. ---- - -# Resource `scaffolding_resource` - -Sample resource in the Terraform provider scaffolding. - -## Example Usage - -```terraform -resource "scaffolding_resource" "example" { - sample_attribute = "foo" -} -``` - -## Schema - -### Optional - -- **id** (String, Optional) The ID of this resource. -- **sample_attribute** (String, Optional) Sample attribute. - - diff --git a/examples/data-sources/scaffolding_data_source/data-source.tf b/examples/data-sources/scaffolding_data_source/data-source.tf deleted file mode 100644 index c4bb102..0000000 --- a/examples/data-sources/scaffolding_data_source/data-source.tf +++ /dev/null @@ -1,3 +0,0 @@ -data "scaffolding_data_source" "example" { - sample_attribute = "foo" -} \ No newline at end of file diff --git a/examples/main.tf b/examples/main.tf new file mode 100644 index 0000000..07e2086 --- /dev/null +++ b/examples/main.tf @@ -0,0 +1,71 @@ +terraform { + required_providers { + mreg = { + version = "0.1.0" + source = "uio.no/usit/mreg" + } + } +} + +provider "mreg" { + serverurl = "https://mreg-test01.example.com/" + token = "1234567890ABCDEF" # replace with actual token +} + +resource "mreg_hosts" "my_hosts" { + # You can supply more than one host in one resource + host { + name = "terraform-provider-test01.example.com" + } + host { + name = "terraform-provider-test02.example.com" + # You can also manually pick an ip address instead of getting assigned a free one + manual_ipaddress = "192.0.2.55" + } + host { + name = "terraform-provider-test03.example.com" + } + contact = "your.email.address@example.com" + comment = "Created by the Terraform provider for Mreg" + network = "192.0.2.0/24" +} + +locals { + hostnames = toset(["test01.terraform-provider-test.example.com", "test02.terraform-provider-test.example.com"]) +} + +resource "mreg_hosts" "loop_hosts" { + # You can loop through a set of hostnames like this + for_each = local.hostnames + host { + name = each.key + } + contact = "your.email.address@example.com" + comment = "Created by the Terraform provider for Mreg" + network = "192.0.2.0/24" +} + +# Here's how to create SRV records +resource "mreg_dns_srv" "srv" { + depends_on = [mreg_hosts.loop_hosts] + for_each = local.hostnames + target_host = each.key + service = "mysql" + proto = "tcp" + name = "terraform-provider-test.example.com" + priority = 0 + weight = 5 + port = 3306 +} + +output "foo" { + value = mreg_hosts.my_hosts +} + +output "bar" { + value = mreg_hosts.loop_hosts +} + +output "baz" { + value = mreg_dns_srv.srv +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf deleted file mode 100644 index 27cbc08..0000000 --- a/examples/provider/provider.tf +++ /dev/null @@ -1,3 +0,0 @@ -provider "scaffolding" { - # example configuration here -} \ No newline at end of file diff --git a/examples/resources/scaffolding_resource/resource.tf b/examples/resources/scaffolding_resource/resource.tf deleted file mode 100644 index 36f1dd0..0000000 --- a/examples/resources/scaffolding_resource/resource.tf +++ /dev/null @@ -1,3 +0,0 @@ -resource "scaffolding_resource" "example" { - sample_attribute = "foo" -} \ No newline at end of file diff --git a/examples/run_example.sh b/examples/run_example.sh new file mode 100755 index 0000000..ca9916d --- /dev/null +++ b/examples/run_example.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e +cd `dirname $0` +pushd .. >/dev/null +rm -f terraform-provider-mreg +go get -v +go build +#TODO after the provider is added to the registry, there's no need to copy the file here +rm -rf ~/.terraform.d/plugins/uio.no/usit/mreg/ +mkdir -p ~/.terraform.d/plugins/uio.no/usit/mreg/0.1.0/linux_amd64 +cp terraform-provider-mreg ~/.terraform.d/plugins/uio.no/usit/mreg/0.1.0/linux_amd64/ +popd >/dev/null +rm -rf .terraform .terraform.lock.hcl terraform.tfstate crash.log +terraform init +terraform apply -auto-approve -parallelism=10 +terraform plan -detailed-exitcode # If there's a diff, the provider is not refreshing the state correctly +terraform destroy -auto-approve +echo Everything works! diff --git a/go.mod b/go.mod index b01f4c5..8ef5a37 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module github.com/hashicorp/terraform-provider-scaffolding +module github.com/unioslo/terraform-provider-mreg go 1.15 require ( github.com/hashicorp/terraform-plugin-docs v0.3.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.4.0 + github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b ) diff --git a/go.sum b/go.sum index 2f614e2..225c66a 100644 --- a/go.sum +++ b/go.sum @@ -227,6 +227,8 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b h1:FQ7+9fxhyp82ks9vAuyPzG0/vVbWwMwLJ+P6yJI5FN8= +github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= diff --git a/internal/provider/data_source_scaffolding.go b/internal/provider/data_source_scaffolding.go deleted file mode 100644 index 6ab8ef3..0000000 --- a/internal/provider/data_source_scaffolding.go +++ /dev/null @@ -1,36 +0,0 @@ -package provider - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceScaffolding() *schema.Resource { - return &schema.Resource{ - // This description is used by the documentation generator and the language server. - Description: "Sample data source in the Terraform provider scaffolding.", - - ReadContext: dataSourceScaffoldingRead, - - Schema: map[string]*schema.Schema{ - "sample_attribute": { - // This description is used by the documentation generator and the language server. - Description: "Sample attribute.", - Type: schema.TypeString, - Required: true, - }, - }, - } -} - -func dataSourceScaffoldingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // use the meta value to retrieve your client from the provider configure method - // client := meta.(*apiClient) - - idFromAPI := "my-id" - d.SetId(idFromAPI) - - return diag.Errorf("not implemented") -} diff --git a/internal/provider/data_source_scaffolding_test.go b/internal/provider/data_source_scaffolding_test.go deleted file mode 100644 index 767dc0c..0000000 --- a/internal/provider/data_source_scaffolding_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package provider - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAccDataSourceScaffolding(t *testing.T) { - t.Skip("data source not yet implemented, remove this once you add your own code") - - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: testAccDataSourceScaffolding, - Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "data.scaffolding_data_source.foo", "sample_attribute", regexp.MustCompile("^ba")), - ), - }, - }, - }) -} - -const testAccDataSourceScaffolding = ` -data "scaffolding_data_source" "foo" { - sample_attribute = "bar" -} -` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a6a941a..771a267 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,6 +2,7 @@ package provider import ( "context" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -26,11 +27,21 @@ func init() { func New(version string) func() *schema.Provider { return func() *schema.Provider { p := &schema.Provider{ - DataSourcesMap: map[string]*schema.Resource{ - "scaffolding_data_source": dataSourceScaffolding(), - }, ResourcesMap: map[string]*schema.Resource{ - "scaffolding_resource": resourceScaffolding(), + "mreg_hosts": resourceHosts(), + "mreg_dns_srv": resourceSRV(), + }, + DataSourcesMap: map[string]*schema.Resource{}, + Schema: map[string]*schema.Schema{ + "serverurl": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "token": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, }, } @@ -44,14 +55,23 @@ type apiClient struct { // Add whatever fields, client or connection info, etc. here // you would need to setup to communicate with the upstream // API. + Serverurl string + Token string +} + +func (c apiClient) UrlWithoutSlash() string { + return strings.TrimSuffix(c.Serverurl, "/") } func configure(version string, p *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) { - return func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) { + return func(c context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { // Setup a User-Agent for your API client (replace the provider name for yours): // userAgent := p.UserAgent("terraform-provider-scaffolding", version) // TODO: myClient.UserAgent = userAgent - return &apiClient{}, nil + return apiClient{ + Serverurl: d.Get("serverurl").(string), + Token: d.Get("token").(string), + }, nil } } diff --git a/internal/provider/resource_hosts.go b/internal/provider/resource_hosts.go new file mode 100644 index 0000000..d30abce --- /dev/null +++ b/internal/provider/resource_hosts.go @@ -0,0 +1,238 @@ +package provider + +import ( + "context" + "crypto/md5" + "fmt" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/juju/fslock" +) + +func resourceHosts() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceHostsCreate, + ReadContext: resourceHostsRead, + DeleteContext: resourceHostsDelete, + Schema: map[string]*schema.Schema{ + "host": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comment": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "contact": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "manual_ipaddress": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comment": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "contact": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceHostsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + apiClient := m.(apiClient) + + hosts := d.Get("host").([]interface{}) + comment := d.Get("comment").(string) + contact := d.Get("contact").(string) + network := d.Get("network").(string) + + lock := fslock.New("terraform-provider-mreg-lockfile") + lock.Lock() + defer lock.Unlock() + + hostnames := make([]string, len(hosts)) + for i := range hosts { + host := hosts[i].(map[string]interface{}) + hostname := host["name"].(string) + hostnames[i] = hostname + + var ipaddress string + + manual_ip := host["manual_ipaddress"].(string) + if manual_ip != "" { + ipaddress = manual_ip + } else { + // Find a free IP address in Mreg + body, _, diags := httpRequest( + "GET", fmt.Sprintf("/api/v1/networks/%s/first_unused", url.QueryEscape(network)), + nil, http.StatusOK, apiClient) + if len(diags) > 0 { + return diags + } + + ipaddress = strings.Trim(body, "\"") + } + + // Use the IP address and allocate a new host object in Mreg + request := map[string]interface{}{ + "name": hostname, + "ipaddress": ipaddress, + "contact": contact, + "comment": comment, + } + _, _, diags := httpRequest("POST", "/api/v1/hosts/", request, http.StatusCreated, apiClient) + if len(diags) > 0 { + return diags + } + + // Update the ResourceData + host["ipaddress"] = ipaddress + host["comment"] = comment + host["contact"] = contact + hosts[i] = host + + d.Set("host", hosts) + d.SetId(hostname) + } + d.Set("host", hosts) + d.SetId(compoundId(hostnames)) + + return diags + +} + +func resourceHostsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + apiClient := m.(apiClient) + + hosts, ok := d.Get("host").([]interface{}) + if !ok { + var diags diag.Diagnostics + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Apparently the TF state doesn't contain any Mreg hosts", + Detail: "", + }) + return diags + } + + hostnames := make([]string, 0, len(hosts)) + for i := 0; i < len(hosts); i++ { + host := hosts[i].(map[string]interface{}) + hostname := host["name"].(string) + + // Read information about this host from Mreg + _, body, diags := httpRequest("GET", "/api/v1/hosts/"+url.QueryEscape(hostname), + nil, http.StatusOK, apiClient) + if len(diags) > 0 { + return diags + } + result := body.(map[string]interface{}) + + // Update the data model with data from Mreg + host["comment"] = result["comment"] + host["ipaddress"] = GetStringFromData(result, "ipaddresses.0.ipaddress") + host["contact"] = result["contact"] + hosts[i] = host + + hostnames = append(hostnames, hostname) + } + + d.Set("host", hosts) + d.SetId(compoundId(hostnames)) + + return diag.Diagnostics{} +} + +func resourceHostsDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + apiClient := m.(apiClient) + d.SetId("") + + hosts, ok := d.Get("host").([]interface{}) + if !ok { + return diag.Diagnostics{} + } + + for i := 0; i < len(hosts); i++ { + host := hosts[i].(map[string]interface{}) + hostname := host["name"].(string) + + // Delete this host from Mreg + _, _, diags := httpRequest("DELETE", "/api/v1/hosts/"+url.QueryEscape(hostname), + nil, http.StatusNoContent, apiClient) + if len(diags) > 0 { + return diags + } + } + + return diag.Diagnostics{} +} + +// compoundId returns an id value that is unique for the given set of hostnames, +// and doesn't depend on the order. +func compoundId(hostnames []string) string { + sort.Strings(hostnames) + hash := md5.New() + for _, s := range hostnames { + hash.Write([]byte(s)) + } + return fmt.Sprintf("%x", hash.Sum(nil)) +} + +// GetStringFromData lets you specify a path to the value that you want +// (e.g. "aaa.bbb.ccc") and have it extracted from the data structure. +func GetStringFromData(v interface{}, path string) string { + for _, key := range strings.Split(path, ".") { + iKey, err := strconv.ParseInt(key, 10, 32) + if err == nil { + // If the key is a number, we assume the structure is an array + arr, ok := v.([]interface{}) + if !ok { + return "" + } + v = arr[iKey] + } else { + // If the key isn't a number, we assume the structure is a map + m, ok := v.(map[string]interface{}) + if !ok { + return "" + } + v = m[key] + } + } + return fmt.Sprintf("%v", v) +} diff --git a/internal/provider/resource_scaffolding.go b/internal/provider/resource_scaffolding.go deleted file mode 100644 index 1ce7cc9..0000000 --- a/internal/provider/resource_scaffolding.go +++ /dev/null @@ -1,60 +0,0 @@ -package provider - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceScaffolding() *schema.Resource { - return &schema.Resource{ - // This description is used by the documentation generator and the language server. - Description: "Sample resource in the Terraform provider scaffolding.", - - CreateContext: resourceScaffoldingCreate, - ReadContext: resourceScaffoldingRead, - UpdateContext: resourceScaffoldingUpdate, - DeleteContext: resourceScaffoldingDelete, - - Schema: map[string]*schema.Schema{ - "sample_attribute": { - // This description is used by the documentation generator and the language server. - Description: "Sample attribute.", - Type: schema.TypeString, - Optional: true, - }, - }, - } -} - -func resourceScaffoldingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // use the meta value to retrieve your client from the provider configure method - // client := meta.(*apiClient) - - idFromAPI := "my-id" - d.SetId(idFromAPI) - - return diag.Errorf("not implemented") -} - -func resourceScaffoldingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // use the meta value to retrieve your client from the provider configure method - // client := meta.(*apiClient) - - return diag.Errorf("not implemented") -} - -func resourceScaffoldingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // use the meta value to retrieve your client from the provider configure method - // client := meta.(*apiClient) - - return diag.Errorf("not implemented") -} - -func resourceScaffoldingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // use the meta value to retrieve your client from the provider configure method - // client := meta.(*apiClient) - - return diag.Errorf("not implemented") -} diff --git a/internal/provider/resource_scaffolding_test.go b/internal/provider/resource_scaffolding_test.go deleted file mode 100644 index 0ebb8d2..0000000 --- a/internal/provider/resource_scaffolding_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package provider - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAccResourceScaffolding(t *testing.T) { - t.Skip("resource not yet implemented, remove this once you add your own code") - - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: testAccResourceScaffolding, - Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "scaffolding_resource.foo", "sample_attribute", regexp.MustCompile("^ba")), - ), - }, - }, - }) -} - -const testAccResourceScaffolding = ` -resource "scaffolding_resource" "foo" { - sample_attribute = "bar" -} -` diff --git a/internal/provider/resource_srv.go b/internal/provider/resource_srv.go new file mode 100644 index 0000000..79a9e93 --- /dev/null +++ b/internal/provider/resource_srv.go @@ -0,0 +1,149 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceSRV() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSRVCreate, + ReadContext: resourceSRVRead, + DeleteContext: resourceSRVDelete, + Schema: map[string]*schema.Schema{ + "target_host": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "service": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "proto": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "priority": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "weight": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceSRVCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + apiClient := m.(apiClient) + + // Find the host ID in Mreg by looking up the hostname + _, body, diags := httpRequest("GET", "/api/v1/hosts/"+url.QueryEscape(d.Get("target_host").(string)), nil, http.StatusOK, apiClient) + if len(diags) > 0 { + return diags + } + result := body.(map[string]interface{}) + hostID := int(result["id"].(float64)) // Go always turns JSON numbers into float64 values + + // assemble the "_service._proto.name."-part of the SRV record + serviceProtoName := fmt.Sprintf("_%s._%s.%s.", d.Get("service").(string), d.Get("proto").(string), d.Get("name").(string)) + + // Create a new SRV record + postdata := map[string]interface{}{ + "name": serviceProtoName, + "priority": d.Get("priority"), + "weight": d.Get("weight"), + "port": d.Get("port"), + "host": hostID, + } + _, _, diags = httpRequest("POST", "/api/v1/srvs/", postdata, http.StatusCreated, apiClient) + if len(diags) > 0 { + return diags + } + + d.SetId(fmt.Sprintf("%s|%d|%d|%d|%d", serviceProtoName, d.Get("priority").(int), d.Get("weight").(int), d.Get("port").(int), hostID)) + return diag.Diagnostics{} +} + +func resourceSRVRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return diag.Diagnostics{} +} + +func resourceSRVDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + apiClient := m.(apiClient) + + // assemble the "_service._proto.name."-part of the SRV record + serviceProtoName := fmt.Sprintf("_%s._%s.%s.", d.Get("service").(string), d.Get("proto").(string), d.Get("name").(string)) + + // Find the host ID in Mreg by looking up the hostname + _, body, diags := httpRequest("GET", "/api/v1/hosts/"+url.QueryEscape(d.Get("target_host").(string)), nil, http.StatusOK, apiClient) + if len(diags) > 0 { + return diags + } + bodyMap := body.(map[string]interface{}) + hostID := int(bodyMap["id"].(float64)) // Go always turns JSON numbers into float64 values + + // Find all SRV records of that particular type, for that particular host + _, body, diags = httpRequest("GET", fmt.Sprintf("/api/v1/srvs/?host=%d&name=%s", hostID, url.QueryEscape(serviceProtoName)), nil, http.StatusOK, apiClient) + if len(diags) > 0 { + return diags + } + + bodyMap = body.(map[string]interface{}) + list := bodyMap["results"].([]interface{}) + + // Look through the SRV records for one that matches the one I'm trying to delete, and extract the ID + priority := d.Get("priority").(int) + weight := d.Get("weight").(int) + port := d.Get("port").(int) + var srvId int + for _, r := range list { + m := r.(map[string]interface{}) + if serviceProtoName == m["name"].(string) && priority == int(m["priority"].(float64)) && + weight == int(m["weight"].(float64)) && port == int(m["port"].(float64)) { + srvId = int(m["id"].(float64)) + break + } + } + if srvId == 0 { + // Not found, but that's no problem really + d.SetId("") + var diags diag.Diagnostics + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Apparently the host doesn't have the SRV record in Mreg", + Detail: serviceProtoName + " , " + d.Get("target_host").(string), + }) + return diags + } + + // Delete the SRV record + _, _, diags = httpRequest("DELETE", fmt.Sprintf("/api/v1/srvs/%d", srvId), nil, http.StatusNoContent, apiClient) + if len(diags) > 0 { + return diags + } + + d.SetId("") + return diag.Diagnostics{} +} diff --git a/internal/provider/utilities.go b/internal/provider/utilities.go new file mode 100644 index 0000000..c814022 --- /dev/null +++ b/internal/provider/utilities.go @@ -0,0 +1,79 @@ +package provider + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" +) + +func httpRequest(method, urlPath string, requestBody map[string]interface{}, expectedStatus int, apiClient apiClient) (bodyText string, body interface{}, diags diag.Diagnostics) { + // Turn the request body structure into JSON + var reqBodyReader io.Reader + if requestBody != nil { + reqbody, err := json.Marshal(requestBody) + if err != nil { + diags = diag.FromErr(err) + return + } + reqBodyReader = bytes.NewReader(reqbody) + } + + // Set up the request + url := apiClient.UrlWithoutSlash() + urlPath + req, err := http.NewRequest(method, url, reqBodyReader) + if err != nil { + diags = diag.FromErr(err) + return + } + req.Header.Add("Authorization", "Token "+apiClient.Token) + if reqBodyReader != nil { + req.Header.Add("Content-Type", "application/json") + } + + // Perform the request + response, err := http.DefaultClient.Do(req) + if err != nil { + diags = diag.FromErr(err) + return + } + defer response.Body.Close() + + // Read the response body + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + diags = diag.FromErr(err) + return + } + bodyText = string(responseBody) + + // Check the status code + if response.StatusCode != expectedStatus { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Got an error message from the MREG API", + Detail: fmt.Sprintf("%s %s\nrequest body: %s\nresponse: http status %d\n%s", + req.Method, url, requestBody, response.StatusCode, bodyText), + }) + return + } + + // Unmarshal the response if it is JSON + if response.Header.Get("Content-Type") == "application/json" { + err = json.Unmarshal(responseBody, &body) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: err.Error(), + Detail: string(responseBody), + }) + return + } + } + + return +} diff --git a/main.go b/main.go index a175a24..7f593cc 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,7 @@ import ( "log" "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" - "github.com/hashicorp/terraform-provider-scaffolding/internal/provider" + "github.com/unioslo/terraform-provider-mreg/internal/provider" ) // Run "go generate" to format example terraform files and generate the docs for the registry/website @@ -38,7 +38,7 @@ func main() { if debugMode { // TODO: update this string with the full name of your provider as used in your configs - err := plugin.Debug(context.Background(), "registry.terraform.io/hashicorp/scaffolding", opts) + err := plugin.Debug(context.Background(), "registry.terraform.io/unioslo/mreg", opts) if err != nil { log.Fatal(err.Error()) } diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl new file mode 100644 index 0000000..d398ad9 --- /dev/null +++ b/templates/index.md.tmpl @@ -0,0 +1,22 @@ +--- +page_title: "Provider: Mreg" +subcategory: "" +description: |- + The Mreg provider provides resources to interact with [Mreg](https://github.com/unioslo/mreg/) through the API. +--- + +# Mreg Provider + +The Mreg provider provides resources to interact with [Mreg](https://github.com/unioslo/mreg/) through the API. + +## Example usage + + provider "mreg" { + serverurl = "https://mreg.example.com/" + token = "1234567890ABCDEF" # substitute with your actual access token + } + +### Required configuration options + +- **serverurl** (String) +- **token** (String, Sensitive)