├── terraform-registry-manifest.json ├── tools.go ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── GNUmakefile ├── main.go ├── docs ├── resources │ └── record.md └── index.md ├── internal ├── provider │ ├── provider_test.go │ ├── utils_test.go │ ├── provider.go │ ├── utils.go │ ├── resource_win_dns_record.go │ └── resource_win_dns_record_test.go ├── dnshelper │ ├── dnshelper.go │ ├── ps_command.go │ └── dns.go └── config │ └── config.go ├── LICENSE ├── .goreleaser.yml ├── README.md ├── go.mod └── go.sum /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["5.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "golang.org/x/vuln/cmd/govulncheck" 8 | _ "honnef.co/go/tools/cmd/staticcheck" 9 | _ "mvdan.cc/gofumpt" 10 | ) 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: fmt 3 | go install 4 | 5 | .PHONY: testacc 6 | testacc: 7 | TF_ACC=1 go test ./... 8 | 9 | .PHONY: fmt 10 | fmt: 11 | go run mvdan.cc/gofumpt -w ./ 12 | 13 | .PHONY: check 14 | check: 15 | go run honnef.co/go/tools/cmd/staticcheck ./... 16 | go run golang.org/x/vuln/cmd/govulncheck ./... 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 9 | "github.com/nrkno/terraform-provider-windns/internal/provider" 10 | ) 11 | 12 | // these will be set by the goreleaser configuration 13 | // to appropriate values for the compiled binary. 14 | var version = "dev" 15 | 16 | func main() { 17 | var debugMode bool 18 | 19 | flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve") 20 | flag.Parse() 21 | opts := &plugin.ServeOpts{ 22 | Debug: debugMode, 23 | 24 | ProviderAddr: "registry.terraform.io/nrkno/windns", 25 | ProviderFunc: provider.Provider(version), 26 | } 27 | 28 | plugin.Serve(opts) 29 | } 30 | -------------------------------------------------------------------------------- /docs/resources/record.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "windns_record Resource - terraform-provider-windns" 4 | subcategory: "" 5 | description: |- 6 | windns_record manages DNS Records in a Windows DNS Server. 7 | --- 8 | 9 | # windns_record (Resource) 10 | 11 | `windns_record` manages DNS Records in a Windows DNS Server. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The name of the dns records. 21 | - `records` (Set of String) A list of records. 22 | - `type` (String) The type of the dns records. (AAAA, A, CNAME, TXT or PTR) 23 | - `zone_name` (String) The zone name for the dns records. 24 | 25 | ### Optional 26 | 27 | - `create_ptr` (Boolean) Create PTR records for requested (A or AAAA) records. 28 | 29 | ### Read-Only 30 | 31 | - `id` (String) The ID of this resource. 32 | 33 | 34 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package provider 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | var ( 13 | testAccProviderFactories map[string]func() (*schema.Provider, error) 14 | testAccProvider *schema.Provider 15 | ) 16 | 17 | func init() { 18 | testAccProvider = Provider("dev")() 19 | testAccProviderFactories = map[string]func() (*schema.Provider, error){ 20 | "windns": func() (*schema.Provider, error) { 21 | return testAccProvider, nil 22 | }, 23 | } 24 | } 25 | 26 | func TestProvider(t *testing.T) { 27 | if err := Provider("dev")().InternalValidate(); err != nil { 28 | t.Fatalf("err: %s", err) 29 | } 30 | } 31 | 32 | func testAccPreCheck(t *testing.T, envVars []string) { 33 | for _, envVar := range envVars { 34 | if val := os.Getenv(envVar); val == "" { 35 | t.Fatalf("%s must be set for acceptance tests to work", envVar) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Norsk rikskringkasting (NRK) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/dnshelper/dnshelper.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package dnshelper 4 | 5 | import ( 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | var recordInputPattern = regexp.MustCompile(`^[a-zA-Z0-9:.\-_]+$`) 14 | 15 | func SanitizeInputString(recordType string, input string) (string, error) { 16 | if recordType == "TXT" { 17 | if len(input) > 255 { 18 | return "", fmt.Errorf("TXT record can only be 255 characters long") 19 | } 20 | return escapePowerShellInput(input), nil 21 | } 22 | 23 | if recordInputPattern.MatchString(input) { 24 | return input, nil 25 | } 26 | return "", fmt.Errorf("invalid characters detected in input: %s", input) 27 | } 28 | 29 | func SanitiseTFInput(d *schema.ResourceData, key string) (string, error) { 30 | return SanitizeInputString(d.Get("type").(string), d.Get(key).(string)) 31 | } 32 | 33 | func escapePowerShellInput(input string) string { 34 | replacer := strings.NewReplacer( 35 | "`", "``", 36 | ";", "`;", 37 | "&", "`&", 38 | "(", "`(", 39 | ")", "`)", 40 | ) 41 | return replacer.Replace(input) 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Terraform Provider release workflow. 2 | name: Release 3 | 4 | # This GitHub action creates a release when a tag that matches the pattern 5 | # "v*" (e.g. v0.1.0) is created. 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | # Releases need permissions to read and write the repository contents. 12 | # GitHub considers creating releases and uploading assets as writing contents. 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | goreleaser: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | # Allow goreleaser to access older tag information. 23 | fetch-depth: 0 24 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 25 | with: 26 | go-version-file: 'go.mod' 27 | cache: true 28 | - name: Import GPG key 29 | uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 30 | id: import_gpg 31 | with: 32 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 33 | passphrase: ${{ secrets.PASSPHRASE }} 34 | - name: Run GoReleaser 35 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 36 | with: 37 | args: release --clean 38 | env: 39 | # GitHub sets the GITHUB_TOKEN secret automatically. 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} -------------------------------------------------------------------------------- /internal/provider/utils_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package provider 4 | 5 | import "testing" 6 | 7 | func Test_suppressRecordDiffForType(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | rrType string 11 | oldRecords []string 12 | newRecords []string 13 | want bool 14 | }{ 15 | // rrType AAAA test cases 16 | { 17 | "test-uppercase-ipv6", "AAAA", []string{"2001:DB8::1"}, []string{"2001:db8::1"}, true, 18 | }, 19 | { 20 | "test-lowercase-ipv6", "AAAA", []string{"2001:db8::1"}, []string{"2001:db8::1"}, true, 21 | }, 22 | { 23 | "test-multiple-unsorted-casemix-ipv6", "AAAA", []string{"2001:DB8::1", "2001:db8::2"}, []string{"2001:db8::2", "2001:db8::1"}, true, 24 | }, 25 | { 26 | "test-multiple-lengths-1", "AAAA", []string{"2001:DB8::1"}, []string{"2001:db8::2", "2001:db8::1"}, false, 27 | }, 28 | { 29 | "test-multiple-lengths-2", "AAAA", []string{}, []string{"2001:db8::2", "2001:db8::1"}, false, 30 | }, 31 | { 32 | "test-empty", "AAAA", []string{}, []string{}, true, 33 | }, 34 | // rrType A test cases 35 | { 36 | "test-empty", "A", []string{}, []string{}, true, 37 | }, 38 | { 39 | "test-ipv4", "A", []string{"203.0.113.11"}, []string{"203.0.113.11"}, true, 40 | }, 41 | { 42 | "test-empty-ivp4", "A", []string{""}, []string{"203.0.113.11"}, false, 43 | }, 44 | // rrType CNAME test cases 45 | { 46 | "test-dot-cname", "CNAME", []string{"example-host.example.com."}, []string{"example-host.example.com"}, true, 47 | }, 48 | // rrType PTR test cases 49 | { 50 | "test-dot-ptr", "PTR", []string{"example-host.example.com."}, []string{"example-host.example.com"}, true, 51 | }, 52 | } 53 | 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | if got := suppressRecordDiffForType(tt.oldRecords, tt.newRecords, tt.rrType); got != tt.want { 57 | t.Errorf("suppressRecordDiffForType() = %v, want %v", got, tt.want) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | version: 2 4 | before: 5 | hooks: 6 | # this is just an example and not a requirement for provider building/publishing 7 | - go mod tidy 8 | builds: 9 | - env: 10 | # goreleaser does not work with CGO, it could also complicate 11 | # usage by users in CI/CD systems like Terraform Cloud where 12 | # they are unable to install libraries. 13 | - CGO_ENABLED=0 14 | mod_timestamp: '{{ .CommitTimestamp }}' 15 | flags: 16 | - -trimpath 17 | ldflags: 18 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 19 | goos: 20 | - freebsd 21 | - windows 22 | - linux 23 | - darwin 24 | goarch: 25 | - amd64 26 | - '386' 27 | - arm 28 | - arm64 29 | ignore: 30 | - goos: darwin 31 | goarch: '386' 32 | binary: '{{ .ProjectName }}_v{{ .Version }}' 33 | archives: 34 | - format: zip 35 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 36 | checksum: 37 | extra_files: 38 | - glob: 'terraform-registry-manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 40 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 41 | algorithm: sha256 42 | signs: 43 | - artifacts: checksum 44 | args: 45 | # if you are using this in a GitHub action or some other automated pipeline, you 46 | # need to pass the batch flag to indicate its not interactive. 47 | - "--batch" 48 | - "--local-user" 49 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 50 | - "--output" 51 | - "${signature}" 52 | - "--detach-sign" 53 | - "${artifact}" 54 | release: 55 | extra_files: 56 | - glob: 'terraform-registry-manifest.json' 57 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 58 | # If you want to manually examine the release before its live, uncomment this line: 59 | # draft: true 60 | changelog: 61 | disable: true 62 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package config 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/melbahja/goph" 10 | ) 11 | 12 | type Settings struct { 13 | SshUsername string 14 | SshPassword string 15 | SshHostname string 16 | DnsServer string 17 | Version string 18 | } 19 | 20 | func NewConfig(d *schema.ResourceData) (*Settings, error) { 21 | sshUsername := d.Get("ssh_username").(string) 22 | sshPassword := d.Get("ssh_password").(string) 23 | sshHost := d.Get("ssh_hostname").(string) 24 | dnsServer := d.Get("dns_server").(string) 25 | 26 | cfg := &Settings{ 27 | SshHostname: sshHost, 28 | SshUsername: sshUsername, 29 | SshPassword: sshPassword, 30 | DnsServer: dnsServer, 31 | } 32 | 33 | return cfg, nil 34 | } 35 | 36 | func GetSSHConnection(settings *Settings) (*goph.Client, error) { 37 | auth := goph.Password(settings.SshPassword) 38 | client, err := goph.NewUnknown(settings.SshUsername, settings.SshHostname, auth) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return client, err 44 | } 45 | 46 | type ProviderConf struct { 47 | Settings *Settings 48 | sshClients []*goph.Client 49 | mx *sync.Mutex 50 | } 51 | 52 | func NewProviderConf(settings *Settings) *ProviderConf { 53 | pcfg := &ProviderConf{ 54 | Settings: settings, 55 | sshClients: make([]*goph.Client, 0), 56 | mx: &sync.Mutex{}, 57 | } 58 | return pcfg 59 | } 60 | 61 | func (c *ProviderConf) AcquireSshClient() (client *goph.Client, err error) { 62 | c.mx.Lock() 63 | defer c.mx.Unlock() 64 | if len(c.sshClients) == 0 { 65 | client, err = GetSSHConnection(c.Settings) 66 | if err != nil { 67 | return nil, err 68 | } 69 | } else { 70 | client = c.sshClients[0] 71 | c.sshClients = c.sshClients[1:] 72 | } 73 | return client, nil 74 | } 75 | 76 | func (c *ProviderConf) ReleaseSshClient(client *goph.Client) { 77 | c.mx.Lock() 78 | defer c.mx.Unlock() 79 | c.sshClients = append(c.sshClients, client) 80 | } 81 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package provider 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/nrkno/terraform-provider-windns/internal/config" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | ) 13 | 14 | // Provider exports the provider schema 15 | func Provider(version string) func() *schema.Provider { 16 | return func() *schema.Provider { 17 | p := &schema.Provider{ 18 | Schema: map[string]*schema.Schema{ 19 | "ssh_username": { 20 | Type: schema.TypeString, 21 | Required: true, 22 | DefaultFunc: schema.EnvDefaultFunc("WINDNS_SSH_USERNAME", ""), 23 | Description: "The username used to authenticate to the server's SSH service. (Environment variable: WINDNS_SSH_USERNAME)", 24 | }, 25 | "ssh_password": { 26 | Type: schema.TypeString, 27 | Required: true, 28 | DefaultFunc: schema.EnvDefaultFunc("WINDNS_SSH_PASSWORD", ""), 29 | Description: "The password used to authenticate to the server's SSH service. (Environment variable: WINDNS_SSH_PASSWORD)", 30 | }, 31 | "ssh_hostname": { 32 | Type: schema.TypeString, 33 | Required: true, 34 | DefaultFunc: schema.EnvDefaultFunc("WINDNS_SSH_HOSTNAME", ""), 35 | Description: "The hostname of the server we will use to run powershell scripts over SSH. (Environment variable: WINDNS_SSH_HOSTNAME)", 36 | }, 37 | "dns_server": { 38 | Type: schema.TypeString, 39 | Optional: true, 40 | DefaultFunc: schema.EnvDefaultFunc("WINDNS_DNS_SERVER_HOSTNAME", ""), 41 | Description: "The hostname of the DNS server. (Environment variable: WINDNS_DNS_SERVER_HOSTNAME)", 42 | }, 43 | }, 44 | DataSourcesMap: map[string]*schema.Resource{}, 45 | ResourcesMap: map[string]*schema.Resource{ 46 | "windns_record": resourceDNSRecord(), 47 | }, 48 | ConfigureContextFunc: providerConfigure, 49 | } 50 | return p 51 | } 52 | } 53 | 54 | func providerConfigure(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { 55 | cfg, err := config.NewConfig(d) 56 | if err != nil { 57 | return nil, diag.FromErr(err) 58 | } 59 | pcfg := config.NewProviderConf(cfg) 60 | return pcfg, nil 61 | } 62 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "windns Provider" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # windns Provider 10 | 11 | This Terraform provider allows you to manage your Windows DNS server resources through Terraform. Currently, it supports 12 | managing records of type `AAAA`, `A`, `CNAME`, `TXT` and `PTR`. 13 | 14 | ## Prerequisites 15 | 16 | This provider requires a remote Windows server exposed with SSH and with the 17 | [DnsService](https://learn.microsoft.com/en-us/powershell/module/dnsserver/?view=windowsserver2022-ps) 18 | PowerShell module installed. This server could be the DNS server itself. 19 | 20 | ## Why use this provider? 21 | Other Terraform providers have implemented similar functionality, but they either require a local Windows installation 22 | running PowerShell or utilize WinRM to execute PowerShell remotely. In many environments, this is not preferable or 23 | possible. 24 | 25 | This, and other similar providers are configuring Windows DNS servers using the PowerShell module 26 | [DnsService](https://learn.microsoft.com/en-us/powershell/module/dnsserver/?view=windowsserver2022-ps). 27 | This module uses WinRM internally when talking to the DNS Server. 28 | 29 | In an environment where the DNS server is running on a locked-down Domain Controller with WinRM disabled, one will thus 30 | run into problems with the second hop WinRM when going through a jump host. We have yet to find a solution to making the second WinRM hop secure and easily. 31 | 32 | This provider avoids the whole second hop concern by using SSH as the transport for the first hop when running PowerShell. 33 | 34 | 35 | ## Schema 36 | 37 | ### Required 38 | 39 | - `ssh_username` (String) The username used to authenticate to the server's SSH service. (Environment variable: WINDNS_SSH_USERNAME) 40 | - `ssh_password` (String) The password used to authenticate to the server's SSH service. (Environment variable: WINDNS_SSH_PASSWORD) 41 | - `ssh_hostname` (String) The hostname of the server we will use to run powershell scripts over SSH. (Environment variable: WINDNS_SSH_HOSTNAME) 42 | 43 | ### Optional 44 | 45 | - `dns_server` (String) The hostname of the DNS server. (Environment variable: WINDNS_DNS_SERVER_HOSTNAME) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform WinDNS Provider 2 | 3 | 4 | This Terraform provider allows you to manage your Windows DNS server resources through Terraform. Currently, it supports 5 | managing records of type `AAAA`, `A`, `CNAME`, `TXT` and `PTR`. 6 | 7 | ## Prerequisites 8 | This provider requires a remote Windows server exposed with SSH and with the 9 | [DnsService](https://learn.microsoft.com/en-us/powershell/module/dnsserver/?view=windowsserver2022-ps) 10 | PowerShell module installed. This server could be the DNS server itself. 11 | 12 | ## Why use this provider? 13 | Other Terraform providers have implemented similar functionality, but they either require a local Windows installation 14 | running PowerShell or utilize WinRM to execute PowerShell remotely. In many environments, this is not preferable or 15 | possible. 16 | 17 | This, and other similar providers are configuring Windows DNS servers using the PowerShell module 18 | [DnsService](https://learn.microsoft.com/en-us/powershell/module/dnsserver/?view=windowsserver2022-ps). 19 | This module uses WinRM internally when talking to the DNS Server. 20 | 21 | In an environment where the DNS server is running on a locked-down Domain Controller with WinRM disabled, one will thus 22 | run into problems with the second hop WinRM when going through a jump host. We have yet to find a solution to making the second WinRM hop secure and easily. 23 | 24 | This provider avoids the whole second hop concern by using SSH as the transport for the first hop when running PowerShell. 25 | 26 | 27 | ## Usage 28 | 29 | Detailed documentation can be found [here](https://registry.terraform.io/providers/nrkno/windns/latest/docs). 30 | 31 | 32 | ### Example 33 | A minimal terraform configuration: 34 | 35 | ``` 36 | terraform { 37 | required_providers { 38 | windns = { 39 | source = "nrkno/windns" 40 | version = "1.0.0" 41 | } 42 | } 43 | } 44 | 45 | provider "windns" { 46 | ssh_username = "someuser" # (environment variable WINDNS_SSH_USERNAME) 47 | ssh_password = "somepassword" # (environment variable WINDNS_SSH_PASSWORD) 48 | ssh_hostname = "somehost" # (environment variable WINDNS_SSH_HOSTNAME) 49 | 50 | # Optional 51 | dns_server = "someserver" # (environment variable WINDNS_DNS_SERVER_HOSTNAME) 52 | } 53 | 54 | resource "windns_record" "r" { 55 | name = "nrk" 56 | zone_name = "example.com" 57 | type = "A" 58 | records = ["203.0.113.11", "203.0.113.12"] 59 | } 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /internal/dnshelper/ps_command.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package dnshelper 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/nrkno/terraform-provider-windns/internal/config" 11 | "golang.org/x/crypto/ssh" 12 | 13 | "github.com/masterzen/winrm" 14 | ) 15 | 16 | type CreatePSCommandOpts struct { 17 | ForceArray bool 18 | JSONOutput bool 19 | JSONDepth int 20 | Password string 21 | Server string 22 | Username string 23 | } 24 | 25 | type PSCommand struct { 26 | CreatePSCommandOpts 27 | cmd string 28 | } 29 | 30 | func NewPSCommand(cmds []string, opts CreatePSCommandOpts) *PSCommand { 31 | cmd := strings.Join(cmds, " ") 32 | 33 | if opts.Server != "" { 34 | cmd = fmt.Sprintf("%s -ComputerName %s", cmd, opts.Server) 35 | } 36 | 37 | if opts.JSONOutput { 38 | cmd = fmt.Sprintf("%s %s", cmd, "| ConvertTo-Json") 39 | if opts.JSONDepth != 0 { 40 | cmd = fmt.Sprintf("%s %s", cmd, fmt.Sprintf("-Depth %d", opts.JSONDepth)) 41 | } 42 | } 43 | 44 | res := PSCommand{ 45 | CreatePSCommandOpts: opts, 46 | cmd: cmd, 47 | } 48 | 49 | return &res 50 | } 51 | 52 | // Run will run a powershell command and return the stdout and stderr 53 | // The output is converted to JSON if the json parameter is set to true. 54 | func (p *PSCommand) Run(conf *config.ProviderConf) (*PSCommandResult, error) { 55 | var ( 56 | err error 57 | exitCode int 58 | stderr bytes.Buffer 59 | stdout bytes.Buffer 60 | ) 61 | conn, err := conf.AcquireSshClient() 62 | if err != nil { 63 | return nil, fmt.Errorf("while acquiring ssh client: %s", err) 64 | } 65 | defer conf.ReleaseSshClient(conn) 66 | 67 | encodedCmd := winrm.Powershell(p.cmd) 68 | 69 | cmd, err := conn.Command(encodedCmd) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | cmd.Session.Stderr = &stderr 75 | cmd.Session.Stdout = &stdout 76 | 77 | err = cmd.Run() 78 | if err != nil { 79 | if v, ok := err.(*ssh.ExitError); ok { 80 | exitCode = v.ExitStatus() 81 | } else { 82 | return nil, fmt.Errorf("run error: %s", err) 83 | } 84 | } 85 | 86 | out := stdout.String() 87 | if p.ForceArray && stdout.String() != "" && stdout.String()[0] != '[' { 88 | out = fmt.Sprintf("[%s]", stdout.String()) 89 | } 90 | 91 | result := &PSCommandResult{ 92 | Stdout: out, 93 | StdErr: stderr.String(), 94 | ExitCode: exitCode, 95 | } 96 | return result, nil 97 | } 98 | 99 | func (p *PSCommand) String() string { 100 | return p.cmd 101 | } 102 | 103 | // PSCommandResult holds the stdout, stderr and exit code of a powershell command 104 | type PSCommandResult struct { 105 | Stdout string 106 | StdErr string 107 | ExitCode int 108 | } 109 | -------------------------------------------------------------------------------- /internal/provider/utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package provider 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/nrkno/terraform-provider-windns/internal/dnshelper" 11 | "golang.org/x/exp/slices" 12 | ) 13 | 14 | func suppressCaseDiff(key, old, new string, d *schema.ResourceData) bool { 15 | // k is ignored here, but wee need to include it in the function's 16 | // signature in order to match the one defined for DiffSuppressFunc 17 | return strings.EqualFold(old, new) 18 | } 19 | 20 | func suppressRecordDiff(key, old, new string, d *schema.ResourceData) bool { 21 | // For a list, the key is path to the element, rather than the list. 22 | // E.g. "windns_record.2.records.0" 23 | lastDotIndex := strings.LastIndex(key, ".") 24 | if lastDotIndex != -1 { 25 | key = key[:lastDotIndex] 26 | } 27 | 28 | oldData, newData := d.GetChange(key) 29 | if oldData == nil || newData == nil { 30 | return false 31 | } 32 | 33 | rrType := d.Get("type").(string) 34 | oldRecords := setToStringSlice(oldData.(*schema.Set)) 35 | newRecords := setToStringSlice(newData.(*schema.Set)) 36 | 37 | // prevent (known after apply) to be ignored 38 | if len(oldRecords) == 0 && len(newRecords) == 0 { 39 | return false 40 | } 41 | 42 | return suppressRecordDiffForType(oldRecords, newRecords, rrType) 43 | } 44 | 45 | func suppressRecordDiffForType(oldRecords, newRecords []string, rrType string) bool { 46 | slices.Sort(oldRecords) 47 | slices.Sort(newRecords) 48 | 49 | if rrType == dnshelper.RecordTypePTR || rrType == dnshelper.RecordTypeCNAME { 50 | return suppressDotDiff(oldRecords, newRecords) 51 | } 52 | return suppressListCaseDiff(oldRecords, newRecords) 53 | } 54 | 55 | // Get-DNSResourceRecord always returns AAAA records in lower case. 56 | // To avoid change if a user used uppercase, we ignore case. 57 | func suppressListCaseDiff(oldRecords, newRecords []string) bool { 58 | return slices.EqualFunc(oldRecords, newRecords, func(old, new string) bool { 59 | return strings.EqualFold(old, new) 60 | }) 61 | } 62 | 63 | // Get-DNSResourceRecord always adds a `.` after the PTR and CNAME record types. 64 | // To avoid change if the user did not add it, we need to add it before we compare. 65 | func suppressDotDiff(oldRecords, newRecords []string) bool { 66 | var newRecordsWithDot []string 67 | 68 | for _, v := range newRecords { 69 | if strings.HasSuffix(v, ".") { 70 | newRecordsWithDot = append(newRecordsWithDot, v) 71 | } else { 72 | newRecordsWithDot = append(newRecordsWithDot, fmt.Sprintf("%s.", v)) 73 | } 74 | } 75 | return slices.Equal(oldRecords, newRecordsWithDot) 76 | } 77 | 78 | func setToStringSlice(d *schema.Set) []string { 79 | var data []string 80 | for _, v := range d.List() { 81 | data = append(data, fmt.Sprintf("%s", v)) 82 | } 83 | return data 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nrkno/terraform-provider-windns 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/hashicorp/terraform-plugin-log v0.9.0 9 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 10 | github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d 11 | github.com/melbahja/goph v1.4.0 12 | golang.org/x/crypto v0.37.0 13 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa 14 | golang.org/x/vuln v1.1.4 15 | honnef.co/go/tools v0.6.1 16 | mvdan.cc/gofumpt v0.8.0 17 | ) 18 | 19 | require ( 20 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 21 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 22 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect 23 | github.com/ProtonMail/go-crypto v1.1.3 // indirect 24 | github.com/agext/levenshtein v1.2.3 // indirect 25 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 26 | github.com/cloudflare/circl v1.3.7 // indirect 27 | github.com/fatih/color v1.16.0 // indirect 28 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 29 | github.com/golang/protobuf v1.5.4 // indirect 30 | github.com/google/go-cmp v0.6.0 // indirect 31 | github.com/hashicorp/errwrap v1.1.0 // indirect 32 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 33 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 34 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect 35 | github.com/hashicorp/go-hclog v1.6.3 // indirect 36 | github.com/hashicorp/go-multierror v1.1.1 // indirect 37 | github.com/hashicorp/go-plugin v1.6.2 // indirect 38 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 39 | github.com/hashicorp/go-uuid v1.0.3 // indirect 40 | github.com/hashicorp/go-version v1.7.0 // indirect 41 | github.com/hashicorp/hc-install v0.9.1 // indirect 42 | github.com/hashicorp/hcl/v2 v2.23.0 // indirect 43 | github.com/hashicorp/logutils v1.0.0 // indirect 44 | github.com/hashicorp/terraform-exec v0.22.0 // indirect 45 | github.com/hashicorp/terraform-json v0.24.0 // indirect 46 | github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect 47 | github.com/hashicorp/terraform-registry-address v0.2.4 // indirect 48 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 49 | github.com/hashicorp/yamux v0.1.1 // indirect 50 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 51 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 52 | github.com/jcmturner/gofork v1.7.6 // indirect 53 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 54 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 55 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 56 | github.com/kr/fs v0.1.0 // indirect 57 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect 58 | github.com/mattn/go-colorable v0.1.13 // indirect 59 | github.com/mattn/go-isatty v0.0.20 // indirect 60 | github.com/mitchellh/copystructure v1.2.0 // indirect 61 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 62 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 63 | github.com/mitchellh/mapstructure v1.5.0 // indirect 64 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 65 | github.com/oklog/run v1.1.0 // indirect 66 | github.com/pkg/errors v0.9.1 // indirect 67 | github.com/pkg/sftp v1.13.5 // indirect 68 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 69 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 70 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 71 | github.com/zclconf/go-cty v1.16.2 // indirect 72 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect 73 | golang.org/x/mod v0.24.0 // indirect 74 | golang.org/x/net v0.39.0 // indirect 75 | golang.org/x/sync v0.13.0 // indirect 76 | golang.org/x/sys v0.32.0 // indirect 77 | golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect 78 | golang.org/x/text v0.24.0 // indirect 79 | golang.org/x/tools v0.32.0 // indirect 80 | google.golang.org/appengine v1.6.8 // indirect 81 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect 82 | google.golang.org/grpc v1.69.4 // indirect 83 | google.golang.org/protobuf v1.36.3 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /internal/provider/resource_win_dns_record.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package provider 4 | 5 | import ( 6 | "context" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | "github.com/nrkno/terraform-provider-windns/internal/config" 13 | "github.com/nrkno/terraform-provider-windns/internal/dnshelper" 14 | ) 15 | 16 | func resourceDNSRecord() *schema.Resource { 17 | return &schema.Resource{ 18 | Description: "`windns_record` manages DNS Records in a Windows DNS Server.", 19 | Importer: &schema.ResourceImporter{ 20 | StateContext: schema.ImportStatePassthroughContext, 21 | }, 22 | ReadContext: resourceDNSRecordRead, 23 | CreateContext: resourceDNSRecordCreate, 24 | UpdateContext: resourceDNSRecordUpdate, 25 | DeleteContext: resourceDNSRecordDelete, 26 | Schema: map[string]*schema.Schema{ 27 | "zone_name": { 28 | Type: schema.TypeString, 29 | Required: true, 30 | DiffSuppressFunc: suppressCaseDiff, 31 | Description: "The zone name for the dns records.", 32 | }, 33 | "name": { 34 | Type: schema.TypeString, 35 | Required: true, 36 | DiffSuppressFunc: suppressCaseDiff, 37 | Description: "The name of the dns records.", 38 | }, 39 | "type": { 40 | Type: schema.TypeString, 41 | Required: true, 42 | DiffSuppressFunc: suppressCaseDiff, 43 | Description: "The type of the dns records.", 44 | }, 45 | "records": { 46 | Type: schema.TypeSet, 47 | Required: true, 48 | Description: "A list of records.", 49 | DiffSuppressFunc: suppressRecordDiff, 50 | Set: schema.HashString, 51 | Elem: &schema.Schema{Type: schema.TypeString}, 52 | MinItems: 1, 53 | }, 54 | "create_ptr": { 55 | Type: schema.TypeBool, 56 | Required: false, 57 | Optional: true, 58 | Description: "Create PTR records for requested (A or AAAA) records.", 59 | }, 60 | }, 61 | CustomizeDiff: customdiff.All( 62 | customdiff.ForceNewIfChange("zone_name", func(ctx context.Context, old, new, meta any) bool { 63 | return new.(string) != old.(string) 64 | }), 65 | customdiff.ForceNewIfChange("name", func(ctx context.Context, old, new, meta any) bool { 66 | return new.(string) != old.(string) 67 | }), 68 | customdiff.ForceNewIfChange("type", func(ctx context.Context, old, new, meta any) bool { 69 | return new.(string) != old.(string) 70 | }), 71 | customdiff.ForceNewIfChange("type", func(ctx context.Context, old, new, meta any) bool { 72 | return new.(string) != old.(string) 73 | }), 74 | ), 75 | } 76 | } 77 | 78 | func resourceDNSRecordCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 79 | record, err := dnshelper.NewDNSRecordFromResource(d) 80 | if err != nil { 81 | return diag.Errorf("error when mapping input data: %s", err) 82 | } 83 | 84 | id, err := record.Create(meta.(*config.ProviderConf)) 85 | if err != nil { 86 | return diag.Errorf("error while creating new record object: %s", err) 87 | } 88 | d.SetId(id) 89 | 90 | return resourceDNSRecordRead(ctx, d, meta) 91 | } 92 | 93 | func resourceDNSRecordRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 94 | if d.Id() == "" { 95 | return nil 96 | } 97 | 98 | record, err := dnshelper.GetDNSRecordFromId(ctx, meta.(*config.ProviderConf), d.Id()) 99 | if err != nil { 100 | if strings.Contains(err.Error(), "ObjectNotFound") { 101 | // Resource no longer exists 102 | d.SetId("") 103 | return nil 104 | } 105 | return diag.Errorf("error while reading record with id %q: %s", d.Id(), err) 106 | } 107 | 108 | _ = d.Set("zone_name", record.ZoneName) 109 | _ = d.Set("name", record.HostName) 110 | _ = d.Set("type", record.RecordType) 111 | _ = d.Set("records", record.Records) 112 | _ = d.Set("create_ptr", record.CreatePtr) 113 | 114 | return nil 115 | } 116 | 117 | func resourceDNSRecordUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 118 | record, err := dnshelper.NewDNSRecordFromResource(d) 119 | if err != nil { 120 | return diag.Errorf("error when mapping input data: %s", err) 121 | } 122 | keys := []string{"records"} 123 | changes := make(map[string]interface{}) 124 | for _, key := range keys { 125 | if d.HasChange(key) { 126 | changes[key] = d.Get(key) 127 | } 128 | } 129 | 130 | err = record.Update(ctx, meta.(*config.ProviderConf), changes) 131 | if err != nil { 132 | return diag.Errorf("error while updating record with id %q: %s", d.Id(), err) 133 | } 134 | return resourceDNSRecordRead(ctx, d, meta) 135 | } 136 | 137 | func resourceDNSRecordDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 138 | if d.Id() == "" { 139 | return nil 140 | } 141 | record, err := dnshelper.NewDNSRecordFromResource(d) 142 | if err != nil { 143 | return diag.Errorf("error when mapping input data: %s", err) 144 | } 145 | 146 | err = record.Delete(meta.(*config.ProviderConf)) 147 | if err != nil { 148 | return diag.Errorf("error while deleting a record object with id %q: %s", d.Id(), err) 149 | } 150 | 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /internal/dnshelper/dns.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package dnshelper 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/hashicorp/terraform-plugin-log/tflog" 14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 15 | "github.com/nrkno/terraform-provider-windns/internal/config" 16 | ) 17 | 18 | const ( 19 | IDSeparator = "_" 20 | 21 | RecordTypeAAAA = "AAAA" 22 | RecordTypeA = "A" 23 | RecordTypeTXT = "TXT" 24 | RecordTypePTR = "PTR" 25 | RecordTypeCNAME = "CNAME" 26 | ) 27 | 28 | type Record struct { 29 | ZoneName string `json:"ZoneName"` 30 | HostName string `json:"HostName"` 31 | RecordType string `json:"RecordType"` 32 | Records []string `json:"Records"` 33 | CreatePtr bool `json:"CreatePtr"` 34 | } 35 | 36 | type DNSRecord struct { 37 | HostName string `json:"HostName"` 38 | RecordType string `json:"RecordType"` 39 | DN string `json:"DistinguishedName"` 40 | RecordData RecordData `json:"RecordData"` 41 | TimeToLive TTL `json:"TimeToLive"` 42 | } 43 | 44 | // The structure we get from powershell contains more fields, but we're only interested in CimInstanceProperties. 45 | type RecordData struct { 46 | CimInstanceProperties []CimInstanceProperties `json:"CimInstanceProperties"` 47 | } 48 | 49 | // The structure we get from powershell contains more fields, but we're only interested in the Value. 50 | type CimInstanceProperties struct { 51 | Value string `json:"value"` 52 | } 53 | 54 | // The structure we get from powershell contains more fields, but we're only interested in TotalSeconds. 55 | type TTL struct { 56 | TotalSeconds int64 `json:"TotalSeconds"` 57 | } 58 | 59 | // windns has no concept of primary key so we need to create one based on inputs 60 | func (r *Record) Id() string { 61 | return strings.Join([]string{r.HostName, r.ZoneName, r.RecordType, strconv.FormatBool(r.CreatePtr)}, IDSeparator) 62 | } 63 | 64 | // NewDNSRecordFromResource returns a new Record struct populated from resource data 65 | func NewDNSRecordFromResource(d *schema.ResourceData) (*Record, error) { 66 | var records []string 67 | recordsSet := d.Get("records").(*schema.Set) 68 | recordType := d.Get("type").(string) 69 | 70 | for _, v := range recordsSet.List() { 71 | sanitizedInput, err := SanitizeInputString(recordType, v.(string)) 72 | if err != nil { 73 | return nil, err 74 | } 75 | records = append(records, sanitizedInput) 76 | } 77 | 78 | sanitizedZoneName, err := SanitiseTFInput(d, "zone_name") 79 | if err != nil { 80 | return nil, err 81 | } 82 | sanitizedHostName, err := SanitiseTFInput(d, "name") 83 | if err != nil { 84 | return nil, err 85 | } 86 | sanitizedRecordType, err := SanitiseTFInput(d, "type") 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return &Record{ 92 | ZoneName: sanitizedZoneName, 93 | HostName: sanitizedHostName, 94 | RecordType: sanitizedRecordType, 95 | CreatePtr: d.Get("create_ptr").(bool), 96 | // TTL: d.Get("ttl").(int64), 97 | Records: records, 98 | }, nil 99 | } 100 | 101 | func GetDNSRecordFromId(ctx context.Context, conf *config.ProviderConf, id string) (*Record, error) { 102 | idComponents := strings.Split(id, IDSeparator) 103 | hostName := idComponents[0] 104 | zoneName := idComponents[1] 105 | recordType := idComponents[2] 106 | createPtr, err := strconv.ParseBool("false") 107 | 108 | if len(idComponents) > 3 { 109 | createPtr, err = strconv.ParseBool(idComponents[3]) 110 | } 111 | if err != nil { 112 | return nil, fmt.Errorf("unknown state for createPtr: %s", err) 113 | } 114 | 115 | // TODO better error handling here. Test import. 116 | 117 | cmd := fmt.Sprintf("Get-DnsServerResourceRecord -ZoneName %s -Name %s -RRType %s", zoneName, hostName, recordType) 118 | 119 | conn, err := conf.AcquireSshClient() 120 | if err != nil { 121 | return nil, fmt.Errorf("while acquiring ssh client: %s", err) 122 | } 123 | defer conf.ReleaseSshClient(conn) 124 | 125 | psOpts := CreatePSCommandOpts{ 126 | JSONOutput: true, 127 | JSONDepth: 4, 128 | ForceArray: true, 129 | Username: conf.Settings.SshUsername, 130 | Password: conf.Settings.SshPassword, 131 | Server: conf.Settings.DnsServer, 132 | } 133 | psCmd := NewPSCommand([]string{cmd}, psOpts) 134 | 135 | result, err := psCmd.Run(conf) 136 | if err != nil { 137 | return nil, fmt.Errorf("ssh execution failure in GetDNSRecordFromId: %s", err) 138 | } 139 | 140 | if result.ExitCode != 0 { 141 | return nil, fmt.Errorf("Get-DnsServerResourceRecord exited with a non zero exit code (%d), stderr: %s", result.ExitCode, result.StdErr) 142 | } 143 | 144 | record, err := unmarshallRecord(ctx, []byte(result.Stdout)) 145 | if err != nil { 146 | return nil, fmt.Errorf("GetDNSRecordFromId: %s", err) 147 | } 148 | 149 | record.ZoneName = zoneName 150 | record.CreatePtr = createPtr 151 | return record, nil 152 | } 153 | 154 | // Create creates a new DNSRecord object in DNS server 155 | func (r *Record) Create(conf *config.ProviderConf) (string, error) { 156 | if r.ZoneName == "" { 157 | return "", fmt.Errorf("DNSRecord.Create: missing zone_name variable") 158 | } 159 | 160 | if r.HostName == "" { 161 | return "", fmt.Errorf("DNSRecord.Create: missing name variable") 162 | } 163 | 164 | if r.RecordType == "" { 165 | return "", fmt.Errorf("DNSRecord.Create: missing type variable") 166 | } 167 | 168 | if len(r.Records) == 0 { 169 | return "", fmt.Errorf("DNSRecord.Create: missing record variable") 170 | } 171 | 172 | for _, recordData := range r.Records { 173 | err := r.addRecordData(conf, recordData) 174 | if err != nil { 175 | return "", err 176 | } 177 | } 178 | 179 | // We don't get any unique ID from the create command, so we assume id is a composite of input variables. 180 | return r.Id(), nil 181 | } 182 | 183 | // Update updates an existing DNSRecord object in DNS server 184 | func (r *Record) Update(ctx context.Context, conf *config.ProviderConf, changes map[string]interface{}) error { 185 | existing, err := GetDNSRecordFromId(ctx, conf, r.Id()) 186 | if err != nil { 187 | return err 188 | } 189 | if changes["records"] == nil { 190 | return nil 191 | } 192 | var records []string 193 | expectedRecords := changes["records"].(*schema.Set) 194 | 195 | for _, v := range expectedRecords.List() { 196 | sanitizedInput, err := SanitizeInputString(existing.RecordType, v.(string)) 197 | if err != nil { 198 | return err 199 | } 200 | records = append(records, sanitizedInput) 201 | } 202 | 203 | toAdd, toRemove := diffRecordLists(records, existing.Records) 204 | for _, recordData := range toAdd { 205 | err = r.addRecordData(conf, recordData) 206 | if err != nil { 207 | return err 208 | } 209 | } 210 | 211 | for _, recordData := range toRemove { 212 | err = r.removeRecordData(conf, recordData) 213 | if err != nil { 214 | return err 215 | } 216 | } 217 | return nil 218 | } 219 | 220 | // Delete deletes an existing DNSRecord object in DNS server 221 | func (r *Record) Delete(conf *config.ProviderConf) error { 222 | for _, recordData := range r.Records { 223 | err := r.removeRecordData(conf, recordData) 224 | if err != nil { 225 | return err 226 | } 227 | } 228 | return nil 229 | } 230 | 231 | func (r *Record) addRecordData(conf *config.ProviderConf, recordData string) error { 232 | cmd := fmt.Sprintf("Add-DNSServerResourceRecord -ZoneName %s -name %s -%s", r.ZoneName, r.HostName, r.RecordType) 233 | 234 | if r.RecordType == RecordTypeA { 235 | cmd = fmt.Sprintf("%s -IPv4Address %s", cmd, recordData) 236 | } else if r.RecordType == RecordTypeAAAA { 237 | cmd = fmt.Sprintf("%s -IPv6Address %s", cmd, strings.ToLower(recordData)) 238 | } else if r.RecordType == RecordTypeTXT { 239 | cmd = fmt.Sprintf("%s -DescriptiveText \"%s\"", cmd, recordData) 240 | } else if r.RecordType == RecordTypePTR { 241 | cmd = fmt.Sprintf("%s -PtrDomainName %s", cmd, recordData) 242 | } else if r.RecordType == RecordTypeCNAME { 243 | cmd = fmt.Sprintf("%s -HostNameAlias %s", cmd, recordData) 244 | } else { 245 | return fmt.Errorf("record type %s is not supported", r.RecordType) 246 | } 247 | 248 | if (r.RecordType == RecordTypeA || r.RecordType == RecordTypeAAAA) && r.CreatePtr { 249 | cmd = fmt.Sprintf("%s -CreatePtr", cmd) 250 | } 251 | 252 | psOpts := CreatePSCommandOpts{ 253 | JSONOutput: false, 254 | ForceArray: false, 255 | Username: conf.Settings.SshUsername, 256 | Password: conf.Settings.SshPassword, 257 | Server: conf.Settings.DnsServer, 258 | } 259 | psCmd := NewPSCommand([]string{cmd}, psOpts) 260 | 261 | result, err := psCmd.Run(conf) 262 | if err != nil { 263 | return fmt.Errorf("ssh execution failure while creating a DNS object: %s", err) 264 | } 265 | 266 | if result.ExitCode != 0 { 267 | fmt.Printf("CMD: %s", psCmd.String()) 268 | return fmt.Errorf("Add-DnsServerResourceRecord exited with a non zero exit code (%d), stderr: %s", result.ExitCode, result.StdErr) 269 | } 270 | return nil 271 | } 272 | 273 | func (r *Record) removeRecordData(conf *config.ProviderConf, recordData string) error { 274 | cmd := fmt.Sprintf("Remove-DnsServerResourceRecord -Force -ZoneName %s -RRType %s -Name %s -RecordData \"%s\"", r.ZoneName, r.RecordType, r.HostName, recordData) 275 | 276 | conn, err := conf.AcquireSshClient() 277 | if err != nil { 278 | return fmt.Errorf("while acquiring ssh client: %s", err) 279 | } 280 | defer conf.ReleaseSshClient(conn) 281 | psOpts := CreatePSCommandOpts{ 282 | JSONOutput: false, 283 | ForceArray: false, 284 | Username: conf.Settings.SshUsername, 285 | Password: conf.Settings.SshPassword, 286 | Server: conf.Settings.DnsServer, 287 | } 288 | 289 | psCmd := NewPSCommand([]string{cmd}, psOpts) 290 | 291 | result, err := psCmd.Run(conf) 292 | if err != nil { 293 | return fmt.Errorf("ssh execution failure while removing record object: %s", err) 294 | } 295 | if result.ExitCode != 0 { 296 | return fmt.Errorf("Remove-DnsServerResourceRecord exited with a non zero exit code (%d), stderr: %s", result.ExitCode, result.StdErr) 297 | } 298 | return nil 299 | } 300 | 301 | // handle if powershell returns single object or list of objects. 302 | func unmarshallRecord(ctx context.Context, input []byte) (*Record, error) { 303 | var err error 304 | var records []DNSRecord 305 | 306 | t := bytes.TrimSpace(input) 307 | if len(t) == 0 { 308 | return nil, fmt.Errorf("empty json document") 309 | } 310 | 311 | err = json.Unmarshal(input, &records) 312 | if err != nil { 313 | tflog.Debug(ctx, fmt.Sprintf("Failed to unmarshall an DNSRecord json document with error %q, document was %s", err, string(input))) 314 | return nil, fmt.Errorf("failed while unmarshalling DNSRecord json document: %s", err) 315 | } 316 | 317 | if len(records) == 0 { 318 | return nil, fmt.Errorf("invalid data while unmarshalling DNSRecord data, json doc was: %s", string(input)) 319 | } 320 | 321 | var rs []string 322 | for _, v := range records { 323 | recordData := v.RecordData.CimInstanceProperties[0].Value 324 | rs = append(rs, recordData) 325 | } 326 | 327 | record := Record{ 328 | HostName: records[0].HostName, 329 | RecordType: records[0].RecordType, 330 | // TTL: records[0].TimeToLive.TotalSeconds, 331 | Records: rs, 332 | } 333 | 334 | return &record, nil 335 | } 336 | 337 | func recordExistsInList(r string, list []string) bool { 338 | for _, item := range list { 339 | if r == item { 340 | return true 341 | } 342 | } 343 | return false 344 | } 345 | 346 | func diffRecordLists(expectedRecords, existingRecords []string) ([]string, []string) { 347 | var toAdd, toRemove []string 348 | 349 | for _, record := range expectedRecords { 350 | if !recordExistsInList(record, existingRecords) { 351 | toAdd = append(toAdd, record) 352 | } 353 | } 354 | 355 | for _, record := range existingRecords { 356 | if !recordExistsInList(record, expectedRecords) { 357 | toRemove = append(toRemove, record) 358 | } 359 | } 360 | return toAdd, toRemove 361 | } 362 | -------------------------------------------------------------------------------- /internal/provider/resource_win_dns_record_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package provider 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 14 | "github.com/nrkno/terraform-provider-windns/internal/config" 15 | "github.com/nrkno/terraform-provider-windns/internal/dnshelper" 16 | ) 17 | 18 | /* 19 | Prerequisites for acceptance tests 20 | 21 | - A Windows DNS server with the following zones configured: 22 | - example.com 23 | - 10.10.in-addr.arpa 24 | - 8.b.d.0.1.0.0.2.ip6.arpa 25 | - A Windows server with SSH enabled and the Powershell DnsServer module installed. 26 | - This could be the same as running the DNS server, or another to jump through. 27 | */ 28 | 29 | const testAccResourceDNSRecordConfigBasicPTR = ` 30 | resource "windns_record" "r1" { 31 | name = "12.113" 32 | zone_name = "10.10.in-addr.arpa" 33 | type = "PTR" 34 | records = ["example-host.example.com."] 35 | } 36 | ` 37 | 38 | const testAccResourceDSRRecordConfigPTRWithoutDot = ` 39 | resource "windns_record" "r1" { 40 | name = "12.113" 41 | zone_name = "10.10.in-addr.arpa" 42 | type = "PTR" 43 | records = ["example-host.example.com"] 44 | } 45 | ` 46 | 47 | const testAccResourceDNSRecordConfigBasicA = ` 48 | variable "windns_record_name" {} 49 | 50 | resource "windns_record" "r1" { 51 | name = var.windns_record_name 52 | zone_name = "example.com" 53 | type = "A" 54 | records = ["203.0.113.11", "203.0.113.12"] 55 | create_ptr = true 56 | } 57 | ` 58 | 59 | const testAccResourceDNSRecordConfigBasicAAAA = ` 60 | variable "windns_record_name" {} 61 | 62 | resource "windns_record" "r1" { 63 | name = var.windns_record_name 64 | zone_name = "example.com" 65 | type = "AAAA" 66 | records = ["2001:db8::1", "2001:db8::2"] 67 | create_ptr = true 68 | } 69 | ` 70 | 71 | const testAccResourceDNSRecordConfigUpperAAAA = ` 72 | variable "windns_record_name" {} 73 | 74 | resource "windns_record" "r1" { 75 | name = var.windns_record_name 76 | zone_name = "example.com" 77 | type = "AAAA" 78 | records = ["2001:DB8::1", "2001:DB8::2"] 79 | } 80 | ` 81 | 82 | const testAccResourceDNSRecordConfigBasicTXT = ` 83 | variable "windns_record_name" {} 84 | 85 | resource "windns_record" "r1" { 86 | name = var.windns_record_name 87 | zone_name = "example.com" 88 | type = "TXT" 89 | records = ["TxTdATa9 &!#$%&'()*+,-./:;<=>?@[]^_{|}~"] 90 | } 91 | ` 92 | 93 | const testAccResourceDNSRecordConfigMultiple = ` 94 | variable "windns_record_name" {} 95 | 96 | resource "windns_record" "r1" { 97 | name = var.windns_record_name 98 | zone_name = "example.com" 99 | type = "A" 100 | records = ["203.0.113.11", "203.0.113.12"] 101 | } 102 | 103 | resource "windns_record" "r2" { 104 | name = var.windns_record_name 105 | zone_name = "example.com" 106 | type = "AAAA" 107 | records = ["2001:db8::1"] 108 | } 109 | 110 | resource "windns_record" "r3" { 111 | name = var.windns_record_name 112 | zone_name = "example.com" 113 | type = "TXT" 114 | records = ["TXTDATA"] 115 | } 116 | ` 117 | 118 | const testAccResourceDNSRecordConfigMultipleUpdated = ` 119 | variable "windns_record_name" {} 120 | 121 | resource "windns_record" "r1" { 122 | name = var.windns_record_name 123 | zone_name = "example.com" 124 | type = "A" 125 | records = ["203.0.113.21", "203.0.113.22"] 126 | } 127 | 128 | resource "windns_record" "r2" { 129 | name = var.windns_record_name 130 | zone_name = "example.com" 131 | type = "AAAA" 132 | records = ["2001:db8::2"] 133 | } 134 | 135 | resource "windns_record" "r3" { 136 | name = var.windns_record_name 137 | zone_name = "example.com" 138 | type = "TXT" 139 | records = ["UPDATED_DATA"] 140 | } 141 | ` 142 | 143 | const testAccResourceDNSRecordConfigCNAME = ` 144 | variable "windns_record_name" {} 145 | 146 | resource "windns_record" "r1" { 147 | name = var.windns_record_name 148 | zone_name = "example.com" 149 | type = "CNAME" 150 | records = ["cname.example.com"] 151 | } 152 | ` 153 | 154 | const testAccResourceDNSRecordConfigIllegalCharacter = ` 155 | variable "windns_record_name" {} 156 | 157 | resource "windns_record" "r1" { 158 | name = var.windns_record_name 159 | zone_name = "example.com;" 160 | type = "CNAME" 161 | records = ["cname.example.com"] 162 | } 163 | ` 164 | 165 | func TestAccResourceDNSRecord_BasicPTR(t *testing.T) { 166 | envVars := []string{"TF_VAR_windns_record_name"} 167 | 168 | resource.Test(t, resource.TestCase{ 169 | PreCheck: func() { testAccPreCheck(t, envVars) }, 170 | ProviderFactories: testAccProviderFactories, 171 | CheckDestroy: resource.ComposeTestCheckFunc( 172 | testAccResourceDNSRecordExists("windns_record.r1", []string{"example-host.example.com."}, dnshelper.RecordTypePTR, false), 173 | ), 174 | Steps: []resource.TestStep{ 175 | { 176 | Config: testAccResourceDNSRecordConfigBasicPTR, 177 | Check: resource.ComposeTestCheckFunc( 178 | testAccResourceDNSRecordExists("windns_record.r1", []string{"example-host.example.com."}, dnshelper.RecordTypePTR, true), 179 | ), 180 | }, 181 | { 182 | ResourceName: "windns_record.r1", 183 | ImportState: true, 184 | ImportStateVerify: true, 185 | }, 186 | }, 187 | }) 188 | } 189 | 190 | func TestAccResourceDSRRecord_BasicPTRWithoutDot(t *testing.T) { 191 | envVars := []string{"TF_VAR_windns_record_name"} 192 | 193 | resource.Test(t, resource.TestCase{ 194 | PreCheck: func() { testAccPreCheck(t, envVars) }, 195 | ProviderFactories: testAccProviderFactories, 196 | CheckDestroy: resource.ComposeTestCheckFunc( 197 | testAccResourceDNSRecordExists("windns_record.r1", []string{"example-host.example.com"}, dnshelper.RecordTypePTR, false), 198 | ), 199 | Steps: []resource.TestStep{ 200 | { 201 | Config: testAccResourceDSRRecordConfigPTRWithoutDot, 202 | Check: resource.ComposeTestCheckFunc( 203 | testAccResourceDNSRecordExists("windns_record.r1", []string{"example-host.example.com"}, dnshelper.RecordTypePTR, true), 204 | ), 205 | }, 206 | { 207 | ResourceName: "windns_record.r1", 208 | ImportState: true, 209 | ImportStateVerify: true, 210 | }, 211 | }, 212 | }) 213 | } 214 | 215 | func TestAccResourceDNSRecord_BasicA(t *testing.T) { 216 | envVars := []string{"TF_VAR_windns_record_name"} 217 | 218 | resource.Test(t, resource.TestCase{ 219 | PreCheck: func() { testAccPreCheck(t, envVars) }, 220 | ProviderFactories: testAccProviderFactories, 221 | CheckDestroy: resource.ComposeTestCheckFunc( 222 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.11", "203.0.113.12"}, dnshelper.RecordTypeA, false), 223 | ), 224 | Steps: []resource.TestStep{ 225 | { 226 | Config: testAccResourceDNSRecordConfigBasicA, 227 | Check: resource.ComposeTestCheckFunc( 228 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.11", "203.0.113.12"}, dnshelper.RecordTypeA, true), 229 | ), 230 | }, 231 | { 232 | ResourceName: "windns_record.r1", 233 | ImportState: true, 234 | ImportStateVerify: true, 235 | }, 236 | }, 237 | }) 238 | } 239 | 240 | func TestAccResourceDNSRecord_BasicAAAA(t *testing.T) { 241 | envVars := []string{"TF_VAR_windns_record_name"} 242 | 243 | resource.Test(t, resource.TestCase{ 244 | PreCheck: func() { testAccPreCheck(t, envVars) }, 245 | ProviderFactories: testAccProviderFactories, 246 | CheckDestroy: resource.ComposeTestCheckFunc( 247 | testAccResourceDNSRecordExists("windns_record.r1", []string{"2001:db8::1", "2001:db8::2"}, dnshelper.RecordTypeAAAA, false), 248 | ), 249 | Steps: []resource.TestStep{ 250 | { 251 | Config: testAccResourceDNSRecordConfigBasicAAAA, 252 | Check: resource.ComposeTestCheckFunc( 253 | testAccResourceDNSRecordExists("windns_record.r1", []string{"2001:db8::1", "2001:db8::2"}, dnshelper.RecordTypeAAAA, true), 254 | ), 255 | }, 256 | { 257 | ResourceName: "windns_record.r1", 258 | ImportState: true, 259 | ImportStateVerify: true, 260 | }, 261 | }, 262 | }) 263 | } 264 | 265 | func TestAccResourceDNSRecord_UpperAAAA(t *testing.T) { 266 | envVars := []string{"TF_VAR_windns_record_name"} 267 | 268 | resource.Test(t, resource.TestCase{ 269 | PreCheck: func() { testAccPreCheck(t, envVars) }, 270 | ProviderFactories: testAccProviderFactories, 271 | CheckDestroy: resource.ComposeTestCheckFunc( 272 | testAccResourceDNSRecordExists("windns_record.r1", []string{"2001:db8::1", "2001:db8::2"}, dnshelper.RecordTypeAAAA, false), 273 | ), 274 | Steps: []resource.TestStep{ 275 | { 276 | Config: testAccResourceDNSRecordConfigUpperAAAA, 277 | Check: resource.ComposeTestCheckFunc( 278 | testAccResourceDNSRecordExists("windns_record.r1", []string{"2001:db8::1", "2001:db8::2"}, dnshelper.RecordTypeAAAA, true), 279 | ), 280 | }, 281 | { 282 | ResourceName: "windns_record.r1", 283 | ImportState: true, 284 | ImportStateVerify: true, 285 | }, 286 | }, 287 | }) 288 | } 289 | 290 | func TestAccResourceDNSRecord_BasicTXT(t *testing.T) { 291 | envVars := []string{"TF_VAR_windns_record_name"} 292 | 293 | resource.Test(t, resource.TestCase{ 294 | PreCheck: func() { testAccPreCheck(t, envVars) }, 295 | ProviderFactories: testAccProviderFactories, 296 | CheckDestroy: resource.ComposeTestCheckFunc( 297 | testAccResourceDNSRecordExists("windns_record.r1", []string{"TxTdATa9 &!#$%&'()*+,-./:;<=>?@[]^_{|}~"}, dnshelper.RecordTypeTXT, false), 298 | ), 299 | Steps: []resource.TestStep{ 300 | { 301 | Config: testAccResourceDNSRecordConfigBasicTXT, 302 | Check: resource.ComposeTestCheckFunc( 303 | testAccResourceDNSRecordExists("windns_record.r1", []string{"TxTdATa9 &!#$%&'()*+,-./:;<=>?@[]^_{|}~"}, dnshelper.RecordTypeTXT, true), 304 | ), 305 | }, 306 | { 307 | ResourceName: "windns_record.r1", 308 | ImportState: true, 309 | ImportStateVerify: true, 310 | }, 311 | }, 312 | }) 313 | } 314 | 315 | func TestAccResourceDNSRecord_Multiple(t *testing.T) { 316 | envVars := []string{"TF_VAR_windns_record_name"} 317 | 318 | resource.Test(t, resource.TestCase{ 319 | PreCheck: func() { testAccPreCheck(t, envVars) }, 320 | ProviderFactories: testAccProviderFactories, 321 | CheckDestroy: resource.ComposeTestCheckFunc( 322 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.11", "203.0.113.12"}, dnshelper.RecordTypeA, false), 323 | testAccResourceDNSRecordExists("windns_record.r2", []string{"2001:db8::1"}, dnshelper.RecordTypeAAAA, false), 324 | testAccResourceDNSRecordExists("windns_record.r3", []string{"TXTDATA"}, dnshelper.RecordTypeTXT, false), 325 | ), 326 | Steps: []resource.TestStep{ 327 | { 328 | Config: testAccResourceDNSRecordConfigMultiple, 329 | Check: resource.ComposeTestCheckFunc( 330 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.11", "203.0.113.12"}, dnshelper.RecordTypeA, true), 331 | testAccResourceDNSRecordExists("windns_record.r2", []string{"2001:db8::1"}, dnshelper.RecordTypeAAAA, true), 332 | testAccResourceDNSRecordExists("windns_record.r3", []string{"TXTDATA"}, dnshelper.RecordTypeTXT, true), 333 | ), 334 | }, 335 | { 336 | ResourceName: "windns_record.r1", 337 | ImportState: true, 338 | ImportStateVerify: true, 339 | }, 340 | { 341 | ResourceName: "windns_record.r2", 342 | ImportState: true, 343 | ImportStateVerify: true, 344 | }, 345 | { 346 | ResourceName: "windns_record.r3", 347 | ImportState: true, 348 | ImportStateVerify: true, 349 | }, 350 | }, 351 | }) 352 | } 353 | 354 | func TestAccResourceDNSRecord_MultipleUpdated(t *testing.T) { 355 | envVars := []string{"TF_VAR_windns_record_name"} 356 | 357 | resource.Test(t, resource.TestCase{ 358 | PreCheck: func() { testAccPreCheck(t, envVars) }, 359 | ProviderFactories: testAccProviderFactories, 360 | CheckDestroy: resource.ComposeTestCheckFunc( 361 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.11", "203.0.113.12"}, dnshelper.RecordTypeA, false), 362 | testAccResourceDNSRecordExists("windns_record.r2", []string{"2001:db8::1"}, dnshelper.RecordTypeAAAA, false), 363 | testAccResourceDNSRecordExists("windns_record.r3", []string{"TXTDATA"}, dnshelper.RecordTypeTXT, false), 364 | ), 365 | Steps: []resource.TestStep{ 366 | { 367 | Config: testAccResourceDNSRecordConfigMultiple, 368 | Check: resource.ComposeTestCheckFunc( 369 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.11", "203.0.113.12"}, dnshelper.RecordTypeA, true), 370 | testAccResourceDNSRecordExists("windns_record.r2", []string{"2001:db8::1"}, dnshelper.RecordTypeAAAA, true), 371 | testAccResourceDNSRecordExists("windns_record.r3", []string{"TXTDATA"}, dnshelper.RecordTypeTXT, true), 372 | ), 373 | }, 374 | { 375 | Config: testAccResourceDNSRecordConfigMultipleUpdated, 376 | Check: resource.ComposeTestCheckFunc( 377 | testAccResourceDNSRecordExists("windns_record.r1", []string{"203.0.113.21", "203.0.113.22"}, dnshelper.RecordTypeA, true), 378 | testAccResourceDNSRecordExists("windns_record.r2", []string{"2001:db8::2"}, dnshelper.RecordTypeAAAA, true), 379 | testAccResourceDNSRecordExists("windns_record.r3", []string{"UPDATED_DATA"}, dnshelper.RecordTypeTXT, true), 380 | ), 381 | }, 382 | }, 383 | }) 384 | } 385 | 386 | func TestAccResourceDNSRecord_CNAME(t *testing.T) { 387 | envVars := []string{"TF_VAR_windns_record_name"} 388 | 389 | resource.Test(t, resource.TestCase{ 390 | PreCheck: func() { testAccPreCheck(t, envVars) }, 391 | ProviderFactories: testAccProviderFactories, 392 | CheckDestroy: resource.ComposeTestCheckFunc( 393 | testAccResourceDNSRecordExists("windns_record.r1", []string{"cname.example.com"}, dnshelper.RecordTypeCNAME, false), 394 | ), 395 | Steps: []resource.TestStep{ 396 | { 397 | Config: testAccResourceDNSRecordConfigCNAME, 398 | Check: resource.ComposeTestCheckFunc( 399 | testAccResourceDNSRecordExists("windns_record.r1", []string{"cname.example.com"}, dnshelper.RecordTypeCNAME, true), 400 | ), 401 | }, 402 | { 403 | ResourceName: "windns_record.r1", 404 | ImportState: true, 405 | ImportStateVerify: true, 406 | }, 407 | }, 408 | }) 409 | } 410 | 411 | func TestAccResourceDNSRecord_IllegalCharacter(t *testing.T) { 412 | envVars := []string{"TF_VAR_windns_record_name"} 413 | 414 | resource.Test(t, resource.TestCase{ 415 | PreCheck: func() { testAccPreCheck(t, envVars) }, 416 | ProviderFactories: testAccProviderFactories, 417 | CheckDestroy: resource.ComposeTestCheckFunc( 418 | testAccResourceDNSRecordExists("windns_record.r1", []string{"cname.example.com"}, dnshelper.RecordTypeCNAME, false), 419 | ), 420 | Steps: []resource.TestStep{ 421 | { 422 | Config: testAccResourceDNSRecordConfigIllegalCharacter, 423 | ExpectError: regexp.MustCompile(".*invalid characters detected in input.*"), 424 | }, 425 | }, 426 | }) 427 | } 428 | 429 | func testAccResourceDNSRecordExists(resource string, expectedRecords []string, expectedRecordType string, expected bool) resource.TestCheckFunc { 430 | ctx := context.Background() 431 | return func(s *terraform.State) error { 432 | rs, ok := s.RootModule().Resources[resource] 433 | if !ok { 434 | return fmt.Errorf("%s key not found in state", resource) 435 | } 436 | 437 | r, err := dnshelper.GetDNSRecordFromId(ctx, testAccProvider.Meta().(*config.ProviderConf), rs.Primary.ID) 438 | if err != nil { 439 | if strings.Contains(err.Error(), "ObjectNotFound") && !expected { 440 | return nil 441 | } 442 | return err 443 | } 444 | 445 | found := suppressRecordDiffForType(r.Records, expectedRecords, expectedRecordType) 446 | 447 | if !found { 448 | return fmt.Errorf("record %s did not contain expected record data. Found %q, Expected %q", r.Id(), r.Records, expectedRecords) 449 | } 450 | return nil 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 4 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 5 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 6 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 7 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 8 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= 9 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= 10 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 11 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 12 | github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= 13 | github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 14 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 15 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 16 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= 17 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 18 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 19 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 20 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 21 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 22 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 23 | github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= 24 | github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 29 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 30 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 31 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 32 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 33 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 35 | github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= 36 | github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= 37 | github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= 38 | github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= 39 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 40 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 41 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 42 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 43 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 44 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 45 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 46 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 47 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 48 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 49 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 50 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 51 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 52 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 53 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 54 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 55 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 56 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 57 | github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= 58 | github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= 59 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 60 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 61 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 62 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 63 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 64 | github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= 65 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 66 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 67 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 68 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 69 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 70 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 71 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 72 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 73 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 74 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 75 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= 76 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 77 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 78 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 79 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 80 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= 81 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 82 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 83 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 84 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 85 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 86 | github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= 87 | github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= 88 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 89 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 90 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 91 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 92 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 93 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 94 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 95 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 96 | github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= 97 | github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= 98 | github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= 99 | github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= 100 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 101 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 102 | github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= 103 | github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= 104 | github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= 105 | github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= 106 | github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= 107 | github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= 108 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 109 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 110 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw= 111 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4= 112 | github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= 113 | github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= 114 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= 115 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= 116 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 117 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 118 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 119 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 120 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 121 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 122 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 123 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 124 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 125 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 126 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 127 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 128 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 129 | github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= 130 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 131 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 132 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 133 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 134 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 135 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 136 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 137 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 138 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 139 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 140 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 141 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 142 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 143 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 144 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 145 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 146 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 147 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= 148 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= 149 | github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d h1:GXlX1g/AjI3/izilmeMvP/aHWYCuwOZXpJsS0XdGVls= 150 | github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d/go.mod h1:Iju3u6NzoTAvjuhsGCZc+7fReNnr/Bd6DsWj3WTokIU= 151 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 152 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 153 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 154 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 155 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 156 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 157 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 158 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 159 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 160 | github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs= 161 | github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68= 162 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 163 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 164 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 165 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 166 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 167 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 168 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 169 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 170 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 171 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 172 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 173 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 174 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 175 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 176 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 177 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 178 | github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= 179 | github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= 180 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 181 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 182 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 183 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 184 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 185 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 186 | github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= 187 | github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= 188 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 189 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 190 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 191 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 192 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 193 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 194 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 195 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 196 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 197 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 198 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 199 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 200 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 201 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 202 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 203 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 204 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 205 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 206 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 207 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 208 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 209 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 210 | github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= 211 | github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 212 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= 213 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= 214 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 215 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 216 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 217 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 218 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 219 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 220 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 221 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 222 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 223 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 224 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 225 | golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 226 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 227 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 228 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 229 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 230 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 231 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 232 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 233 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= 234 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 235 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 236 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 237 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 238 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 239 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 242 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 243 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 244 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 245 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 246 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 247 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 248 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 249 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 250 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 251 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 252 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 253 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 254 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 255 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 261 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 262 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 264 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 266 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 267 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 268 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 269 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 270 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 271 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 272 | golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0= 273 | golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= 274 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 275 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 276 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 277 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 278 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 279 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 280 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 281 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 282 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 283 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 284 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 285 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 286 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 287 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 288 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 289 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 290 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 291 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 292 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 293 | golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= 294 | golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= 295 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 296 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 297 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 298 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 299 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 300 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= 301 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 302 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 303 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 304 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 305 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 306 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 307 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 308 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 309 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 310 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 311 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 312 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 313 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 314 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 315 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 316 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 317 | honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= 318 | honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= 319 | mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k= 320 | mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg= 321 | --------------------------------------------------------------------------------