├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── goreleaser.yaml │ ├── release.yaml │ └── test.yaml ├── .release-please-manifest.json ├── examples ├── provider │ ├── variables.tf │ └── provider.tf └── resources │ └── ohdear_site │ ├── import.sh │ └── resource.tf ├── tools.go ├── internal ├── provider │ ├── config.go │ ├── utils_test.go │ ├── utils.go │ ├── provider_test.go │ ├── provider.go │ ├── resource_monitor.go │ ├── resource_site.go │ ├── resource_monitor_test.go │ └── resource_site_test.go └── runtime │ └── runtime.go ├── templates └── index.md.tmpl ├── .gitignore ├── pkg └── ohdear │ ├── logger.go │ ├── error.go │ ├── logger_test.go │ ├── site.go │ ├── monitor.go │ ├── checks.go │ ├── client.go │ ├── checks_test.go │ ├── error_test.go │ ├── site_test.go │ ├── monitor_test.go │ └── client_test.go ├── main.go ├── docs ├── index.md └── resources │ └── site.md ├── LICENSE ├── .goreleaser.yaml ├── release-please-config.json ├── .golangci.yaml ├── README.md ├── Makefile ├── go.mod ├── CHANGELOG.md └── go.sum /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @articulate/devex 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.0.2" 3 | } 4 | -------------------------------------------------------------------------------- /examples/provider/variables.tf: -------------------------------------------------------------------------------- 1 | variable "token" { 2 | } 3 | 4 | variable "api_url" { 5 | } 6 | 7 | variable "team_id" { 8 | } 9 | -------------------------------------------------------------------------------- /examples/resources/ohdear_site/import.sh: -------------------------------------------------------------------------------- 1 | # import using the Site ID found on the settings page 2 | terraform import ohdear_site.test 1234 3 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | // document generation 7 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/provider/config.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/articulate/terraform-provider-ohdear/pkg/ohdear" 5 | ) 6 | 7 | type Config struct { 8 | client *ohdear.Client 9 | teamID int 10 | } 11 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "ohdear" { 2 | api_token = var.token # optionally use OHDEAR_TOKEN env var 3 | api_url = var.api_url # optionally use OHDEAR_API_URL env var 4 | team_id = var.team_id # optionally use OHDEAR_TEAM_ID env var 5 | } 6 | -------------------------------------------------------------------------------- /examples/resources/ohdear_site/resource.tf: -------------------------------------------------------------------------------- 1 | resource "ohdear_site" "test" { 2 | url = "https://example.com" 3 | # all checks are enabled 4 | } 5 | 6 | resource "ohdear_site" "uptime-only" { 7 | url = "https://example.org" 8 | 9 | # Only the uptime check is enabled 10 | checks { 11 | uptime = true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/index.md.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Oh Dear Provider 3 | description: |- 4 | Monitor sites with Oh Dear. 5 | --- 6 | 7 | # Oh Dear Provider 8 | 9 | Setup monitors with [Oh Dear](https://ohdear.app/). 10 | 11 | ## Example Usage 12 | 13 | {{ tffile "examples/provider/provider.tf" }} 14 | 15 | {{ .SchemaMarkdown | trimspace }} 16 | -------------------------------------------------------------------------------- /internal/provider/utils_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import "testing" 4 | 5 | func TestContains(t *testing.T) { 6 | if !contains([]string{"foo", "bar", "baz"}, "bar") { 7 | t.Fatal("exepected to find \"bar\" in [foo, bar, baz]") 8 | } 9 | 10 | if contains([]string{"foo", "bar", "baz"}, "foobar") { 11 | t.Fatal("expected not to find \"foobar\" in [foo, bar, baz]") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/provider/utils.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | ) 8 | 9 | func contains(s []string, e string) bool { 10 | for _, a := range s { 11 | if a == e { 12 | return true 13 | } 14 | } 15 | 16 | return false 17 | } 18 | 19 | func diagErrorf(err error, format string, a ...interface{}) diag.Diagnostics { 20 | return diag.Diagnostics{ 21 | diag.Diagnostic{ 22 | Severity: diag.Error, 23 | Summary: fmt.Sprintf(format, a...), 24 | Detail: err.Error(), 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | .DS_Store 4 | example.tf 5 | terraform.tfplan 6 | terraform.tfstate 7 | bin/ 8 | dist/ 9 | modules-dev/ 10 | website/.vagrant 11 | website/.bundle 12 | website/build 13 | website/node_modules 14 | .vagrant/ 15 | *.backup 16 | ./*.tfstate 17 | .terraform/ 18 | .terraform.lock.hcl 19 | versions.tf 20 | *.log 21 | *.bak 22 | *~ 23 | .*.swp 24 | .idea 25 | *.iml 26 | *.test 27 | *.iml 28 | 29 | website/vendor 30 | 31 | # Test exclusions 32 | !command/test-fixtures/**/*.tfstate 33 | !command/test-fixtures/**/.terraform/ 34 | 35 | # Keep windows files with windows line endings 36 | *.winfile eol=crlf 37 | -------------------------------------------------------------------------------- /pkg/ohdear/logger.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | type TerraformLogger struct{} 9 | 10 | func (l *TerraformLogger) log(level, message string) { 11 | log.Printf("[%s] %s\n", level, message) 12 | } 13 | 14 | func (l *TerraformLogger) Errorf(format string, v ...interface{}) { 15 | l.log("ERROR", fmt.Sprintf(format, v...)) 16 | } 17 | 18 | func (l *TerraformLogger) Warnf(format string, v ...interface{}) { 19 | l.log("WARN", fmt.Sprintf(format, v...)) 20 | } 21 | 22 | func (l *TerraformLogger) Debugf(format string, v ...interface{}) { 23 | l.log("DEBUG", fmt.Sprintf(format, v...)) 24 | } 25 | -------------------------------------------------------------------------------- /internal/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "flag" 5 | "runtime/debug" 6 | ) 7 | 8 | // automatically set by goreleaser 9 | var ( 10 | Version = "DEV" 11 | Commit string 12 | ) 13 | 14 | // Flag (-debug) 15 | var isDebug bool 16 | 17 | func init() { 18 | if Version == "DEV" { 19 | if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" { 20 | Version = info.Main.Version 21 | } 22 | } 23 | 24 | flag.BoolVar(&isDebug, "debug", false, "set to true to run the provider with support for debuggers") 25 | } 26 | 27 | func Debug() bool { 28 | if !flag.Parsed() { 29 | flag.Parse() 30 | } 31 | 32 | return isDebug 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 5 | 6 | "github.com/articulate/terraform-provider-ohdear/internal/provider" 7 | "github.com/articulate/terraform-provider-ohdear/internal/runtime" 8 | ) 9 | 10 | // Format example Terraform files 11 | //go:generate terraform fmt -recursive ./examples/ 12 | 13 | // Run the docs generation tool 14 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 15 | 16 | func main() { 17 | plugin.Serve(&plugin.ServeOpts{ 18 | ProviderFunc: provider.New, 19 | ProviderAddr: "registry.terraform.io/articulate/ohdear", 20 | Debug: runtime.Debug(), 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | commit-message: 8 | prefix: ci 9 | groups: 10 | actions: 11 | patterns: 12 | - "*" 13 | - package-ecosystem: gomod 14 | directory: / 15 | open-pull-requests-limit: 25 16 | schedule: 17 | interval: monthly 18 | commit-message: 19 | prefix: deps 20 | prefix-development: build(gomod) 21 | groups: 22 | terraform: 23 | patterns: 24 | - github.com/hashicorp/* 25 | test: 26 | patterns: 27 | - github.com/jarcoal/httpmock 28 | - github.com/stretchr/testify 29 | -------------------------------------------------------------------------------- /pkg/ohdear/error.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-resty/resty/v2" 8 | ) 9 | 10 | type Error struct { 11 | Response *resty.Response `json:"-"` 12 | Message string 13 | Errors map[string][]string 14 | } 15 | 16 | func (e Error) Error() string { 17 | if e.Message != "" { 18 | msg := e.Message 19 | for key, err := range e.Errors { 20 | msg = fmt.Sprintf("%s\n%s: %s", msg, key, strings.Join(err, " ")) 21 | } 22 | return msg 23 | } 24 | 25 | return fmt.Sprintf("%d: %s", e.Response.StatusCode(), e.Response.Status()) 26 | } 27 | 28 | func errorFromResponse(_ *resty.Client, r *resty.Response) error { 29 | if !r.IsError() { 30 | return nil 31 | } 32 | 33 | err := r.Error().(*Error) 34 | err.Response = r 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /pkg/ohdear/logger_test.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func mocklog() (*bytes.Buffer, func()) { 12 | original := log.Writer() 13 | 14 | output := &bytes.Buffer{} 15 | log.SetOutput(output) 16 | 17 | return output, func() { 18 | log.SetOutput(original) 19 | } 20 | } 21 | 22 | func TestTerraformLogger(t *testing.T) { 23 | logger := &TerraformLogger{} 24 | out, reset := mocklog() 25 | t.Cleanup(reset) 26 | 27 | logger.Errorf("test error message") 28 | assert.Contains(t, out.String(), "[ERROR] test error message\n", out.String()) 29 | 30 | logger.Warnf("test warn message") 31 | assert.Contains(t, out.String(), "[WARN] test warn message\n", out.String()) 32 | 33 | logger.Debugf("test debug message") 34 | assert.Contains(t, out.String(), "[DEBUG] test debug message\n", out.String()) 35 | } 36 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Oh Dear Provider 3 | description: |- 4 | Monitor sites with Oh Dear. 5 | --- 6 | 7 | # Oh Dear Provider 8 | 9 | Setup monitors with [Oh Dear](https://ohdear.app/). 10 | 11 | ## Example Usage 12 | 13 | ```terraform 14 | provider "ohdear" { 15 | api_token = var.token # optionally use OHDEAR_TOKEN env var 16 | api_url = var.api_url # optionally use OHDEAR_API_URL env var 17 | team_id = var.team_id # optionally use OHDEAR_TEAM_ID env var 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `api_token` (String) Oh Dear API token. If not set, uses `OHDEAR_TOKEN` env var. 27 | 28 | ### Optional 29 | 30 | - `api_url` (String) Oh Dear API URL. If not set, uses `OHDEAR_API_URL` env var. Defaults to `https://ohdear.app`. 31 | - `team_id` (Number) The default team ID to use for sites. If not set, uses `OHDEAR_TEAM_ID` env var. 32 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v5 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-go@v6 16 | with: 17 | go-version-file: go.mod 18 | check-latest: true 19 | cache: true 20 | - name: Import GPG key 21 | id: import_gpg 22 | uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # pin@v5 23 | with: 24 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 25 | passphrase: ${{ secrets.PASSPHRASE }} 26 | - uses: goreleaser/goreleaser-action@v6 27 | with: 28 | args: release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 32 | -------------------------------------------------------------------------------- /pkg/ohdear/site.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import "fmt" 4 | 5 | type Site struct { 6 | ID int 7 | URL string 8 | TeamID int `json:"team_id"` 9 | Checks []Check 10 | } 11 | 12 | func (c *Client) GetSite(id int) (*Site, error) { 13 | resp, err := c.R(). 14 | SetResult(&Site{}). 15 | Get(fmt.Sprintf("/api/monitors/%d", id)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return resp.Result().(*Site), nil 21 | } 22 | 23 | func (c *Client) AddSite(url string, teamID int, checks []string) (*Site, error) { 24 | resp, err := c.R(). 25 | SetBody(map[string]interface{}{ 26 | "url": url, 27 | "type": "http", 28 | "team_id": teamID, 29 | "checks": checks, 30 | }). 31 | SetResult(&Site{}). 32 | Post("/api/monitors") 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return resp.Result().(*Site), nil 38 | } 39 | 40 | func (c *Client) RemoveSite(id int) error { 41 | _, err := c.R().Delete(fmt.Sprintf("/api/monitors/%d", id)) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /pkg/ohdear/monitor.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import "fmt" 4 | 5 | type Monitor struct { 6 | ID int 7 | URL string 8 | TeamID int `json:"team_id"` 9 | Checks []Check 10 | } 11 | 12 | func (c *Client) GetMonitor(id int) (*Monitor, error) { 13 | resp, err := c.R(). 14 | SetResult(&Monitor{}). 15 | Get(fmt.Sprintf("/api/monitors/%d", id)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return resp.Result().(*Monitor), nil 21 | } 22 | 23 | func (c *Client) AddMonitor(url string, teamID int, checks []string) (*Monitor, error) { 24 | resp, err := c.R(). 25 | SetBody(map[string]interface{}{ 26 | "url": url, 27 | "type": "http", 28 | "team_id": teamID, 29 | "checks": checks, 30 | }). 31 | SetResult(&Monitor{}). 32 | Post("/api/monitors") 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return resp.Result().(*Monitor), nil 38 | } 39 | 40 | func (c *Client) RemoveMonitor(id int) error { 41 | _, err := c.R().Delete(fmt.Sprintf("/api/monitors/%d", id)) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /pkg/ohdear/checks.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import "fmt" 4 | 5 | const ( 6 | UptimeCheck = "uptime" 7 | BrokenLinksCheck = "broken_links" 8 | CertificateHealthCheck = "certificate_health" 9 | CertificateTransparencyCheck = "certificate_transparency" 10 | MixedContentCheck = "mixed_content" 11 | PerformanceCheck = "performance" 12 | DNSCheck = "dns" 13 | ) 14 | 15 | var AllChecks = []string{ 16 | UptimeCheck, 17 | BrokenLinksCheck, 18 | CertificateHealthCheck, 19 | MixedContentCheck, 20 | PerformanceCheck, 21 | DNSCheck, 22 | } 23 | 24 | type Check struct { 25 | ID int `json:"id"` 26 | Type string `json:"type"` 27 | Enabled bool `json:"enabled"` 28 | } 29 | 30 | func (c *Client) EnableCheck(id int) error { 31 | _, err := c.R().Post(fmt.Sprintf("/api/checks/%d/enable", id)) 32 | return err 33 | } 34 | 35 | func (c *Client) DisableCheck(id int) error { 36 | _, err := c.R().Post(fmt.Sprintf("/api/checks/%d/disable", id)) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /pkg/ohdear/client.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/go-resty/resty/v2" 8 | ) 9 | 10 | type Client struct { 11 | *resty.Client 12 | } 13 | 14 | func NewClient(baseURL, token string) *Client { 15 | client := resty.New() 16 | client.SetBaseURL(baseURL) 17 | client.SetAuthToken(token) 18 | client.SetHeaders(map[string]string{ 19 | "Accept": "application/json", 20 | "Content-Type": "application/json", 21 | }) 22 | client.SetError(&Error{}) 23 | client.OnAfterResponse(errorFromResponse) 24 | 25 | client.SetRetryCount(3) 26 | client.SetRetryWaitTime(5 * time.Second) 27 | client.SetRetryMaxWaitTime(20 * time.Second) 28 | client.AddRetryCondition(func(r *resty.Response, _ error) bool { 29 | return r.StatusCode() == http.StatusTooManyRequests 30 | }) 31 | 32 | client.SetDebug(true) 33 | client.SetLogger(&TerraformLogger{}) 34 | 35 | return &Client{client} 36 | } 37 | 38 | func (c *Client) SetUserAgent(ua string) *Client { 39 | c.Header.Set("User-Agent", ua) 40 | return c 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Articulate Global, LLC 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 | -------------------------------------------------------------------------------- /pkg/ohdear/checks_test.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/jarcoal/httpmock" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEnableCheck(t *testing.T) { 11 | _, reset := mocklog() 12 | t.Cleanup(reset) 13 | t.Cleanup(httpmock.DeactivateAndReset) 14 | 15 | resp, err := httpmock.NewJsonResponder(200, map[string]interface{}{"id": 1234}) 16 | require.NoError(t, err) 17 | httpmock.RegisterResponder("POST", "https://ohdear.test/api/checks/1234/enable", resp) 18 | 19 | client := NewClient("https://ohdear.test", "") 20 | httpmock.ActivateNonDefault(client.GetClient()) 21 | 22 | err = client.EnableCheck(1234) 23 | require.NoError(t, err) 24 | } 25 | 26 | func TestDisableCheck(t *testing.T) { 27 | _, reset := mocklog() 28 | t.Cleanup(reset) 29 | t.Cleanup(httpmock.DeactivateAndReset) 30 | 31 | resp, err := httpmock.NewJsonResponder(200, map[string]interface{}{"id": 4321}) 32 | require.NoError(t, err) 33 | httpmock.RegisterResponder("POST", "https://ohdear.test/api/checks/4321/disable", resp) 34 | 35 | client := NewClient("https://ohdear.test", "") 36 | httpmock.ActivateNonDefault(client.GetClient()) 37 | 38 | err = client.DisableCheck(4321) 39 | require.NoError(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | mod_timestamp: '{{ .CommitTimestamp }}' 9 | flags: 10 | - -trimpath 11 | ldflags: 12 | - -s -w 13 | - -X github.com/articulate/terraform-provider-ohdear/internal/runtime.Version={{.Version}} 14 | - -X github.com/articulate/terraform-provider-ohdear/internal/runtime.Commit={{.Commit}} 15 | goos: 16 | - freebsd 17 | - windows 18 | - linux 19 | - darwin 20 | goarch: 21 | - amd64 22 | - '386' 23 | - arm 24 | - arm64 25 | ignore: 26 | - goos: darwin 27 | goarch: '386' 28 | binary: '{{ .ProjectName }}_v{{ .Version }}' 29 | archives: 30 | - format: zip 31 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 32 | checksum: 33 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 34 | algorithm: sha256 35 | signs: 36 | - artifacts: checksum 37 | args: 38 | - "--batch" 39 | - "--local-user" 40 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 41 | - "--output" 42 | - "${signature}" 43 | - "--detach-sign" 44 | - "${artifact}" 45 | release: 46 | use_existing_draft: true 47 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 9 | ) 10 | 11 | var ( 12 | testAccProvider *schema.Provider 13 | testAccProviderFactories = map[string]func() (*schema.Provider, error){} 14 | ) 15 | 16 | func init() { 17 | testAccProvider = New() 18 | testAccProviderFactories = map[string]func() (*schema.Provider, error){ 19 | "ohdear": func() (*schema.Provider, error) { 20 | return New(), nil 21 | }, 22 | } 23 | } 24 | 25 | func TestProvider(t *testing.T) { 26 | provider := New() 27 | if err := provider.InternalValidate(); err != nil { 28 | t.Fatalf("err: %s", err) 29 | } 30 | } 31 | 32 | func TestProvider_impl(_ *testing.T) { 33 | var _ schema.Provider = *New() //nolint:staticcheck 34 | } 35 | 36 | func testAccPreCheck(t *testing.T) { 37 | if v := os.Getenv("OHDEAR_TOKEN"); v == "" { 38 | t.Fatal("OHDEAR_TOKEN must be set for acceptance tests") 39 | } 40 | if teamID == "" { 41 | t.Fatal("OHDEAR_TEAM_ID must be set for acceptance tests") 42 | } 43 | 44 | diags := testAccProvider.Configure(t.Context(), terraform.NewResourceConfigRaw(nil)) 45 | if diags.HasError() { 46 | t.Fatal(diags[0].Summary) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "include-v-in-tag": true, 4 | "include-component-in-tag": false, 5 | "pull-request-title-pattern": "chore: release ${version}", 6 | "pull-request-header": ":robot: I detected a new release. Please review the changes below.", 7 | "pull-request-footer": "When you are ready to release these changes, merge this PR.", 8 | "changelog-sections": [ 9 | { "type": "feat", "section": "Features" }, 10 | { "type": "fix", "section": "Bug Fixes" }, 11 | { "type": "perf", "section": "Performance Improvements" }, 12 | { "type": "revert", "section": "Reverts" }, 13 | { "type": "deps", "section": "Dependency Updates" }, 14 | { "type": "docs", "section": "Documentation", "hidden": true }, 15 | { "type": "style", "section": "Styles", "hidden": true }, 16 | { "type": "chore", "section": "Miscellaneous", "hidden": true }, 17 | { "type": "refactor", "section": "Refactors" }, 18 | { "type": "test", "section": "Tests", "hidden": true }, 19 | { "type": "build", "section": "Build", "hidden": true }, 20 | { "type": "ci", "section": "Continuous Integration", "hidden": true } 21 | ], 22 | "packages": { 23 | ".": { 24 | "release-type": "go", 25 | "draft": true, 26 | "prerelease": true, 27 | "extra-files": ["README.md"] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/create-github-app-token@v2 13 | id: token 14 | with: 15 | app-id: ${{ secrets.GH_APP_ID }} 16 | private-key: ${{ secrets.GH_PRIVATE_KEY }} 17 | - uses: googleapis/release-please-action@v4 18 | id: release 19 | with: 20 | token: ${{ steps.token.outputs.token }} 21 | skip-github-pull-request: "${{ contains(github.event.head_commit.message, 'chore: release') }}" 22 | - if: steps.release.outputs.release_created 23 | uses: actions/github-script@v8 24 | with: 25 | github-token: ${{ steps.token.outputs.token }} 26 | script: | 27 | const tag = '${{ steps.release.outputs.tag_name }}'; 28 | 29 | // Create annotated tag 30 | const result = await github.rest.git.createTag({ 31 | ...context.repo, 32 | tag, 33 | message: `Release ${tag}`, 34 | type: 'commit', 35 | object: context.sha, 36 | }); 37 | const sha = result.data.sha; 38 | 39 | // Create the ref 40 | await github.rest.git.createRef({ 41 | ...context.repo, 42 | sha, 43 | ref: `refs/tags/${tag}`, 44 | }); 45 | 46 | core.setOutput('sha', sha); 47 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asasalint 5 | - asciicheck 6 | - bidichk 7 | - bodyclose 8 | - contextcheck 9 | - durationcheck 10 | - err113 11 | - errchkjson 12 | - errorlint 13 | - exhaustive 14 | - fatcontext 15 | - gocheckcompilerdirectives 16 | - gochecksumtype 17 | - gocyclo 18 | - gosec 19 | - gosmopolitan 20 | - lll 21 | - loggercheck 22 | - makezero 23 | - misspell 24 | - musttag 25 | - nilerr 26 | - nilnesserr 27 | - noctx 28 | - prealloc 29 | - protogetter 30 | - reassign 31 | - recvcheck 32 | - revive 33 | - spancheck 34 | - testifylint 35 | - usetesting 36 | - whitespace 37 | - zerologlint 38 | disable: 39 | - perfsprint 40 | - rowserrcheck 41 | - sqlclosecheck 42 | - wrapcheck 43 | settings: 44 | gocyclo: 45 | min-complexity: 10 46 | exclusions: 47 | generated: lax 48 | presets: 49 | - comments 50 | - common-false-positives 51 | - legacy 52 | - std-error-handling 53 | rules: 54 | - linters: 55 | - err113 56 | path: _test\.go 57 | paths: 58 | - third_party$ 59 | - builtin$ 60 | - examples$ 61 | formatters: 62 | enable: 63 | - gofmt 64 | - gofumpt 65 | - goimports 66 | settings: 67 | goimports: 68 | local-prefixes: 69 | - github.com/articulate/terraform-provider-ohdear 70 | exclusions: 71 | generated: lax 72 | paths: 73 | - third_party$ 74 | - builtin$ 75 | - examples$ 76 | -------------------------------------------------------------------------------- /pkg/ohdear/error_test.go: -------------------------------------------------------------------------------- 1 | package ohdear 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/go-resty/resty/v2" 8 | "github.com/jarcoal/httpmock" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestError(t *testing.T) { 14 | t.Run("error with message and errors", func(t *testing.T) { 15 | err := &Error{ 16 | Message: "test error", 17 | Errors: map[string][]string{ 18 | "foo": {"bar", "baz"}, 19 | }, 20 | } 21 | 22 | require.EqualError(t, err, "test error\nfoo: bar baz") 23 | }) 24 | 25 | t.Run("error with no errors", func(t *testing.T) { 26 | err := &Error{ 27 | Message: "test error", 28 | } 29 | 30 | require.EqualError(t, err, "test error") 31 | }) 32 | 33 | t.Run("error with no message", func(t *testing.T) { 34 | err := &Error{ 35 | Response: &resty.Response{ 36 | RawResponse: &http.Response{ 37 | Status: "Unauthorized", 38 | StatusCode: 401, 39 | }, 40 | }, 41 | } 42 | 43 | require.EqualError(t, err, "401: Unauthorized") 44 | }) 45 | } 46 | 47 | func TestErrorFromResponse(t *testing.T) { 48 | _, reset := mocklog() 49 | t.Cleanup(reset) 50 | t.Cleanup(httpmock.DeactivateAndReset) 51 | 52 | resp, err := httpmock.NewJsonResponder(404, map[string]interface{}{"message": "Not found"}) 53 | require.NoError(t, err) 54 | httpmock.RegisterResponder("GET", "https://ohdear.test/api/monitors/1", resp) 55 | 56 | client := NewClient("https://ohdear.test", "") 57 | httpmock.ActivateNonDefault(client.GetClient()) 58 | 59 | _, err = client.R().Get("/api/monitors/1") 60 | require.Error(t, err) 61 | 62 | var e *Error 63 | require.ErrorAs(t, err, &e) 64 | assert.Equal(t, 404, e.Response.StatusCode()) 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider OhDear 2 | 3 | A Terraform Provider for [Oh Dear](https://ohdear.app/). 4 | 5 | ## Usage 6 | 7 | The provider requires an `api_token` (or `OHDEAR_TOKEN` environment variable) and 8 | an optional `team_id` (`OHDEAR_TEAM_ID` environment variable). 9 | 10 | 11 | ```hcl 12 | terraform { 13 | required_providers { 14 | ohdear = { 15 | source = "articulate/ohdear" 16 | version = "3.0.2" 17 | } 18 | } 19 | } 20 | 21 | provider "ohdear" { 22 | api_token = "my-api-token" 23 | team_id = 1234 # optional 24 | } 25 | ``` 26 | 27 | 28 | To add a monitor to Oh Dear, create a `ohdear_monitor` resource. 29 | 30 | ```hcl 31 | resource "ohdear_monitor" "test" { 32 | url = "https://monitor.iwanttomonitor.com" 33 | } 34 | ``` 35 | 36 | By default, all checks are enabled. You can customize this using the `checks` 37 | block. Any checks not defined in the block are disabled. 38 | 39 | ```hcl 40 | resource "ohdear_monitor" "test" { 41 | url = "https://monitor.iwanttomonitor.com" 42 | 43 | checks { 44 | uptime = true 45 | } 46 | } 47 | ``` 48 | 49 | ## Development Requirements 50 | 51 | * [Go](https://golang.org/doc/install) (for development) 52 | * [golangci-lint](https://golangci-lint.run/) 53 | * [GoReleaser](https://goreleaser.com/) 54 | 55 | ## Contributing 56 | 57 | Commit messages must be signed and follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 58 | format. 59 | 60 | ## Publishing 61 | 62 | Releases are automatically created by [release-please](https://github.com/googleapis/release-please) 63 | on PR merge. This will scan commit messages for new releases based on commit message 64 | and create a release PR. To finish the release, merge the PR, which will kick off 65 | GoReleaser. 66 | -------------------------------------------------------------------------------- /docs/resources/site.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "ohdear_site Resource - terraform-provider-ohdear" 4 | subcategory: "" 5 | description: |- 6 | ohdear_site manages a site in Oh Dear. 7 | --- 8 | 9 | # ohdear_site (Resource) 10 | 11 | `ohdear_site` manages a site in Oh Dear. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "ohdear_site" "test" { 17 | url = "https://example.com" 18 | # all checks are enabled 19 | } 20 | 21 | resource "ohdear_site" "uptime-only" { 22 | url = "https://example.org" 23 | 24 | # Only the uptime check is enabled 25 | checks { 26 | uptime = true 27 | } 28 | } 29 | ``` 30 | 31 | 32 | ## Schema 33 | 34 | ### Required 35 | 36 | - `url` (String) URL of the site to be checked. 37 | 38 | ### Optional 39 | 40 | - `checks` (Block List, Max: 1) Set the checks enabled for the site. If block is not present, it will enable all checks. (see [below for nested schema](#nestedblock--checks)) 41 | - `team_id` (Number) ID of the team for this site. If not set, will use `team_id` configured in provider. 42 | 43 | ### Read-Only 44 | 45 | - `id` (String) The ID of this resource. 46 | 47 | 48 | ### Nested Schema for `checks` 49 | 50 | Optional: 51 | 52 | - `broken_links` (Boolean) Enable broken link checks. 53 | - `certificate_health` (Boolean) Enable certificate health checks. Requires the url to use https. 54 | - `certificate_transparency` (Boolean) Enable certificate transparency checks. Requires the url to use https. 55 | - `dns` (Boolean) Enable DNS checks. Defaults to `false`. 56 | - `mixed_content` (Boolean) Enable mixed content checks. 57 | - `performance` (Boolean) Enable performance checks. 58 | - `uptime` (Boolean) Enable uptime checks. 59 | 60 | ## Import 61 | 62 | Import is supported using the following syntax: 63 | 64 | ```shell 65 | # import using the Site ID found on the settings page 66 | terraform import ohdear_site.test 1234 67 | ``` 68 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG_LIST := $(shell go list ./... | grep -v /vendor/) 2 | OS_ARCH=$(shell go env GOOS)_$(shell go env GOARCH) 3 | HOSTNAME=registry.terraform.io 4 | NAMESPACE=articulate 5 | NAME=ohdear 6 | VERSION=$(shell git describe --abbrev=0 | sed 's/^v//') 7 | 8 | help: 9 | @echo "+ $@" 10 | @grep -hE '(^[a-zA-Z0-9\._-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m## /[33m/' 11 | .PHONY: help 12 | 13 | ## 14 | ## Build 15 | ## --------------------------------------------------------------------------- 16 | 17 | build: ## Build for current OS/Arch 18 | @echo "+ $@" 19 | @goreleaser build --clean --skip=validate --single-target 20 | .PHONY: build 21 | 22 | all: ## Build all OS/Arch 23 | @echo "+ $@" 24 | @goreleaser build --clean --skip=validate 25 | .PHONY: all 26 | 27 | install: build ## Install to global Terraform plugin directory 28 | @echo "+ $@" 29 | @mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} 30 | @mv dist/terraform-provider-${NAME}_${OS_ARCH}/terraform-provider-* ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} 31 | .PHONY: install 32 | 33 | generate: ## Autogenerate docs and resources 34 | @echo "+ $@" 35 | @go generate ${PKG_LIST} 36 | .PHONY: generate 37 | 38 | ## 39 | ## Development 40 | ## --------------------------------------------------------------------------- 41 | 42 | mod: ## Make sure go.mod is up to date 43 | @echo "+ $@" 44 | @go mod tidy 45 | .PHONY: mod 46 | 47 | lint: ## Lint Go code 48 | @echo "+ $@" 49 | @golangci-lint run 50 | .PHONY: lint 51 | 52 | format: ## Try to fix lint issues 53 | @echo "+ $@" 54 | @golangci-lint run --fix 55 | .PHONY: format 56 | 57 | ## 58 | ## Tests 59 | ## --------------------------------------------------------------------------- 60 | 61 | test: ## Run tests 62 | @echo "+ $@" 63 | @go test ${PKG_LIST} -v $(TESTARGS) -parallel=4 64 | .PHONY: test 65 | 66 | testacc: ## Run acceptance tests 67 | @echo "+ $@" 68 | @TF_ACC=1 go test ${PKG_LIST} -v -cover $(TESTARGS) -timeout 120m 69 | .PHONY: testacc 70 | 71 | 72 | # Print the value of any variable as make print-VAR 73 | print-% : ; @echo $* = $($*) 74 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "README.md" 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - "README.md" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | steps: 18 | - uses: actions/checkout@v5 19 | - uses: actions/setup-go@v6 20 | with: 21 | go-version-file: go.mod 22 | check-latest: true 23 | cache: true 24 | - run: go build -v . 25 | 26 | lint: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v5 30 | - uses: actions/setup-go@v6 31 | with: 32 | go-version-file: go.mod 33 | check-latest: true 34 | cache: true 35 | - uses: golangci/golangci-lint-action@v8 36 | 37 | versions: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Get Terraform versions 41 | id: versions 42 | run: | 43 | echo "terraform=$(curl -sL https://releases.hashicorp.com/terraform/index.json | jq -c '[ 44 | .versions | keys | .[] | 45 | { version: ., semver: capture("(?[0-9]+).(?[0-9]+).(?[0-9]+)(?
.*)") } |
46 |             select(.semver.pre == "") |
47 |             {
48 |                 version: .version,
49 |                 group: (.semver.major + "." + .semver.minor),
50 |                 major: .semver.major | tonumber,
51 |                 minor: .semver.minor | tonumber,
52 |                 patch: .semver.patch | tonumber
53 |             }
54 |           ] | group_by(.group) | [ .[] | sort_by(.patch) | .[-1] | .version ] |
55 |           sort_by(.|split(".")|map(tonumber)) | .[-2:]')" >> "$GITHUB_OUTPUT"
56 |     outputs:
57 |       terraform: ${{ steps.versions.outputs.terraform }}
58 | 
59 |   test:
60 |     runs-on: ubuntu-latest
61 |     needs: versions
62 |     timeout-minutes: 15
63 |     strategy:
64 |       fail-fast: false
65 |       max-parallel: 1
66 |       matrix:
67 |         terraform: ${{ fromJSON(needs.versions.outputs.terraform) }}
68 |     concurrency:
69 |       group: test
70 |       cancel-in-progress: false
71 |     steps:
72 |       - uses: actions/checkout@v5
73 |       - uses: actions/setup-go@v6
74 |         with:
75 |           go-version-file: go.mod
76 |           check-latest: true
77 |           cache: true
78 |       - run: make testacc
79 |         env:
80 |           TF_ACC_TERRAFORM_VERSION: ${{ matrix.terraform }}
81 |           OHDEAR_TOKEN: ${{ secrets.OHDEAR_TOKEN }}
82 |           OHDEAR_TEAM_ID: 6944
83 | 


--------------------------------------------------------------------------------
/internal/provider/provider.go:
--------------------------------------------------------------------------------
 1 | package provider
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"strings"
 7 | 
 8 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 9 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
11 | 
12 | 	"github.com/articulate/terraform-provider-ohdear/internal/runtime"
13 | 	"github.com/articulate/terraform-provider-ohdear/pkg/ohdear"
14 | )
15 | 
16 | func init() {
17 | 	schema.DescriptionKind = schema.StringMarkdown
18 | 
19 | 	// add defaults on to the exported descriptions if present
20 | 	schema.SchemaDescriptionBuilder = func(s *schema.Schema) string {
21 | 		desc := s.Description
22 | 		if s.Default != nil {
23 | 			desc += fmt.Sprintf(" Defaults to `%v`.", s.Default)
24 | 		}
25 | 		if s.Deprecated != "" {
26 | 			desc += " __Deprecated__: " + s.Deprecated
27 | 		}
28 | 		return strings.TrimSpace(desc)
29 | 	}
30 | }
31 | 
32 | func New() *schema.Provider {
33 | 	return &schema.Provider{
34 | 		Schema: map[string]*schema.Schema{
35 | 			"api_token": {
36 | 				Type:        schema.TypeString,
37 | 				Required:    true,
38 | 				Description: "Oh Dear API token. If not set, uses `OHDEAR_TOKEN` env var.",
39 | 				DefaultFunc: schema.EnvDefaultFunc("OHDEAR_TOKEN", nil),
40 | 			},
41 | 			"api_url": {
42 | 				Type:         schema.TypeString,
43 | 				Optional:     true,
44 | 				Description:  "Oh Dear API URL. If not set, uses `OHDEAR_API_URL` env var. Defaults to `https://ohdear.app`.",
45 | 				ValidateFunc: validation.IsURLWithHTTPorHTTPS,
46 | 				DefaultFunc:  schema.EnvDefaultFunc("OHDEAR_API_URL", "https://ohdear.app"),
47 | 			},
48 | 			"team_id": {
49 | 				Type:        schema.TypeInt,
50 | 				Optional:    true,
51 | 				Description: "The default team ID to use for sites. If not set, uses `OHDEAR_TEAM_ID` env var.",
52 | 				DefaultFunc: schema.EnvDefaultFunc("OHDEAR_TEAM_ID", 0),
53 | 			},
54 | 		},
55 | 		ResourcesMap: map[string]*schema.Resource{
56 | 			"ohdear_site":    resourceOhdearSite(),
57 | 			"ohdear_monitor": resourceOhdearMonitor(),
58 | 		},
59 | 		ConfigureContextFunc: providerConfigure,
60 | 	}
61 | }
62 | 
63 | func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
64 | 	ua := fmt.Sprintf(
65 | 		"terraform-provider-ohdear/%s (https://github.com/articulate/terraform-provider-ohdear)",
66 | 		runtime.Version,
67 | 	)
68 | 	client := ohdear.NewClient(d.Get("api_url").(string), d.Get("api_token").(string))
69 | 	client.SetUserAgent(ua)
70 | 
71 | 	return &Config{
72 | 		client: client,
73 | 		teamID: d.Get("team_id").(int),
74 | 	}, nil
75 | }
76 | 


--------------------------------------------------------------------------------
/pkg/ohdear/site_test.go:
--------------------------------------------------------------------------------
  1 | package ohdear
  2 | 
  3 | import (
  4 | 	"io"
  5 | 	"net/http"
  6 | 	"testing"
  7 | 
  8 | 	"github.com/jarcoal/httpmock"
  9 | 	"github.com/stretchr/testify/assert"
 10 | 	"github.com/stretchr/testify/require"
 11 | )
 12 | 
 13 | func TestGetSite(t *testing.T) {
 14 | 	_, reset := mocklog()
 15 | 	t.Cleanup(reset)
 16 | 	t.Cleanup(httpmock.DeactivateAndReset)
 17 | 
 18 | 	resp, err := httpmock.NewJsonResponder(200, map[string]interface{}{
 19 | 		"id":      1234,
 20 | 		"type":    "http",
 21 | 		"url":     "https://example.com",
 22 | 		"team_id": 5678,
 23 | 		"checks": []map[string]interface{}{
 24 | 			{
 25 | 				"id":      12,
 26 | 				"type":    "uptime",
 27 | 				"enabled": true,
 28 | 			},
 29 | 			{
 30 | 				"id":      34,
 31 | 				"type":    "performance",
 32 | 				"enabled": false,
 33 | 			},
 34 | 		},
 35 | 	})
 36 | 	require.NoError(t, err)
 37 | 	httpmock.RegisterResponder("GET", "https://ohdear.test/api/monitors/1234", resp)
 38 | 
 39 | 	client := NewClient("https://ohdear.test", "")
 40 | 	httpmock.ActivateNonDefault(client.GetClient())
 41 | 
 42 | 	site, err := client.GetSite(1234)
 43 | 	require.NoError(t, err)
 44 | 	assert.Equal(t, 1234, site.ID)
 45 | 	assert.Equal(t, "https://example.com", site.URL)
 46 | 	assert.Equal(t, 5678, site.TeamID)
 47 | 	assert.Len(t, site.Checks, 2)
 48 | 	assert.Equal(t, 12, site.Checks[0].ID)
 49 | 	assert.Equal(t, "uptime", site.Checks[0].Type)
 50 | 	assert.True(t, site.Checks[0].Enabled)
 51 | 	assert.Equal(t, "performance", site.Checks[1].Type)
 52 | 	assert.False(t, site.Checks[1].Enabled)
 53 | }
 54 | 
 55 | func TestAddSite(t *testing.T) {
 56 | 	_, reset := mocklog()
 57 | 	t.Cleanup(reset)
 58 | 	t.Cleanup(httpmock.DeactivateAndReset)
 59 | 
 60 | 	httpmock.RegisterResponder("POST", "https://ohdear.test/api/monitors",
 61 | 		func(req *http.Request) (*http.Response, error) {
 62 | 			body, err := io.ReadAll(req.Body)
 63 | 			require.NoError(t, err)
 64 | 			assert.JSONEq(
 65 | 				t,
 66 | 				`{"checks":["uptime","performance","broken_links"],"team_id":5678,"url":"https://example.com/new","type":"http"}`,
 67 | 				string(body),
 68 | 			)
 69 | 
 70 | 			return httpmock.NewJsonResponse(200, map[string]interface{}{
 71 | 				"id":      4321,
 72 | 				"type":    "http",
 73 | 				"url":     "https://example.com/new",
 74 | 				"team_id": 5678,
 75 | 				"checks": []map[string]interface{}{
 76 | 					{
 77 | 						"id":      12,
 78 | 						"type":    "uptime",
 79 | 						"enabled": true,
 80 | 					},
 81 | 				},
 82 | 			})
 83 | 		})
 84 | 
 85 | 	client := NewClient("https://ohdear.test", "")
 86 | 	httpmock.ActivateNonDefault(client.GetClient())
 87 | 
 88 | 	site, err := client.AddSite("https://example.com/new", 5678, []string{"uptime", "performance", "broken_links"})
 89 | 	require.NoError(t, err)
 90 | 	assert.Equal(t, 4321, site.ID)
 91 | 	assert.Equal(t, "https://example.com/new", site.URL)
 92 | }
 93 | 
 94 | func TestRemoveSite(t *testing.T) {
 95 | 	_, reset := mocklog()
 96 | 	t.Cleanup(reset)
 97 | 	t.Cleanup(httpmock.DeactivateAndReset)
 98 | 
 99 | 	httpmock.RegisterResponder("DELETE", "https://ohdear.test/api/monitors/1234", httpmock.NewStringResponder(204, ""))
100 | 
101 | 	client := NewClient("https://ohdear.test", "")
102 | 	httpmock.ActivateNonDefault(client.GetClient())
103 | 
104 | 	err := client.RemoveSite(1234)
105 | 	require.NoError(t, err)
106 | }
107 | 


--------------------------------------------------------------------------------
/pkg/ohdear/monitor_test.go:
--------------------------------------------------------------------------------
  1 | package ohdear
  2 | 
  3 | import (
  4 | 	"io"
  5 | 	"net/http"
  6 | 	"testing"
  7 | 
  8 | 	"github.com/jarcoal/httpmock"
  9 | 	"github.com/stretchr/testify/assert"
 10 | 	"github.com/stretchr/testify/require"
 11 | )
 12 | 
 13 | func TestGetMonitor(t *testing.T) {
 14 | 	_, reset := mocklog()
 15 | 	t.Cleanup(reset)
 16 | 	t.Cleanup(httpmock.DeactivateAndReset)
 17 | 
 18 | 	resp, err := httpmock.NewJsonResponder(200, map[string]interface{}{
 19 | 		"id":      1234,
 20 | 		"url":     "https://example.com",
 21 | 		"team_id": 5678,
 22 | 		"checks": []map[string]interface{}{
 23 | 			{
 24 | 				"id":      12,
 25 | 				"type":    "uptime",
 26 | 				"enabled": true,
 27 | 			},
 28 | 			{
 29 | 				"id":      34,
 30 | 				"type":    "performance",
 31 | 				"enabled": false,
 32 | 			},
 33 | 		},
 34 | 	})
 35 | 	require.NoError(t, err)
 36 | 	httpmock.RegisterResponder("GET", "https://ohdear.test/api/monitors/1234", resp)
 37 | 
 38 | 	client := NewClient("https://ohdear.test", "")
 39 | 	httpmock.ActivateNonDefault(client.GetClient())
 40 | 
 41 | 	monitor, err := client.GetMonitor(1234)
 42 | 	require.NoError(t, err)
 43 | 	assert.Equal(t, 1234, monitor.ID)
 44 | 	assert.Equal(t, "https://example.com", monitor.URL)
 45 | 	assert.Equal(t, 5678, monitor.TeamID)
 46 | 	assert.Len(t, monitor.Checks, 2)
 47 | 	assert.Equal(t, 12, monitor.Checks[0].ID)
 48 | 	assert.Equal(t, "uptime", monitor.Checks[0].Type)
 49 | 	assert.True(t, monitor.Checks[0].Enabled)
 50 | 	assert.Equal(t, "performance", monitor.Checks[1].Type)
 51 | 	assert.False(t, monitor.Checks[1].Enabled)
 52 | }
 53 | 
 54 | func TestAddMonitor(t *testing.T) {
 55 | 	_, reset := mocklog()
 56 | 	t.Cleanup(reset)
 57 | 	t.Cleanup(httpmock.DeactivateAndReset)
 58 | 
 59 | 	httpmock.RegisterResponder("POST", "https://ohdear.test/api/monitors",
 60 | 		func(req *http.Request) (*http.Response, error) {
 61 | 			body, err := io.ReadAll(req.Body)
 62 | 			require.NoError(t, err)
 63 | 			assert.JSONEq(
 64 | 				t,
 65 | 				`{"checks":["uptime","performance","broken_links"],"team_id":5678,"url":"https://example.com/new","type":"http"}`,
 66 | 				string(body),
 67 | 			)
 68 | 
 69 | 			return httpmock.NewJsonResponse(200, map[string]interface{}{
 70 | 				"id":      4321,
 71 | 				"url":     "https://example.com/new",
 72 | 				"team_id": 5678,
 73 | 				"checks": []map[string]interface{}{
 74 | 					{
 75 | 						"id":      12,
 76 | 						"type":    "uptime",
 77 | 						"enabled": true,
 78 | 					},
 79 | 				},
 80 | 			})
 81 | 		})
 82 | 
 83 | 	client := NewClient("https://ohdear.test", "")
 84 | 	httpmock.ActivateNonDefault(client.GetClient())
 85 | 
 86 | 	monitor, err := client.AddMonitor("https://example.com/new", 5678, []string{"uptime", "performance", "broken_links"})
 87 | 	require.NoError(t, err)
 88 | 	assert.Equal(t, 4321, monitor.ID)
 89 | 	assert.Equal(t, "https://example.com/new", monitor.URL)
 90 | }
 91 | 
 92 | func TestRemoveMonitor(t *testing.T) {
 93 | 	_, reset := mocklog()
 94 | 	t.Cleanup(reset)
 95 | 	t.Cleanup(httpmock.DeactivateAndReset)
 96 | 
 97 | 	httpmock.RegisterResponder("DELETE", "https://ohdear.test/api/monitors/1234", httpmock.NewStringResponder(204, ""))
 98 | 
 99 | 	client := NewClient("https://ohdear.test", "")
100 | 	httpmock.ActivateNonDefault(client.GetClient())
101 | 
102 | 	err := client.RemoveMonitor(1234)
103 | 	require.NoError(t, err)
104 | }
105 | 


--------------------------------------------------------------------------------
/pkg/ohdear/client_test.go:
--------------------------------------------------------------------------------
  1 | package ohdear
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"net/http"
  6 | 	"os"
  7 | 	"strconv"
  8 | 	"testing"
  9 | 	"time"
 10 | 
 11 | 	"github.com/jarcoal/httpmock"
 12 | 	"github.com/stretchr/testify/assert"
 13 | 	"github.com/stretchr/testify/require"
 14 | )
 15 | 
 16 | func TestClient(t *testing.T) {
 17 | 	token := os.Getenv("OHDEAR_TOKEN")
 18 | 	teamID := os.Getenv("OHDEAR_TEAM_ID")
 19 | 	if token == "" || teamID == "" || os.Getenv("TF_ACC") == "" {
 20 | 		t.Skip("Integration tests skipped unless 'OHDEAR_TOKEN', 'OHDEAR_TEAM_ID', and 'TF_ACC' env vars are set")
 21 | 	}
 22 | 
 23 | 	if os.Getenv("SKIP_INTEGRATION_TESTS") != "" {
 24 | 		t.Skip("Integration tests skipped because SKIP_INTEGRATION_TESTS env var set")
 25 | 	}
 26 | 
 27 | 	team, err := strconv.Atoi(teamID)
 28 | 	require.NoError(t, err)
 29 | 
 30 | 	url := os.Getenv("OHDEAR_API_URL")
 31 | 	if url == "" {
 32 | 		url = "https://ohdear.app"
 33 | 	}
 34 | 
 35 | 	_, reset := mocklog()
 36 | 	// if we defer, we get log leakage from the other cleanup function
 37 | 	t.Cleanup(reset)
 38 | 
 39 | 	ua := "terraform-provider-ohdear/TEST (https://github.com/articulate/terraform-provider-ohdear) integration-tests"
 40 | 	client := NewClient(url, token)
 41 | 	client.SetDebug(false)
 42 | 	client.SetUserAgent(ua)
 43 | 
 44 | 	create, err := client.AddSite("https://example.com", team, []string{"uptime"})
 45 | 	require.NoError(t, err)
 46 | 
 47 | 	// make sure we remove the site even if tests fail
 48 | 	t.Cleanup(func() {
 49 | 		var e *Error
 50 | 		if !errors.As(err, &e) {
 51 | 			t.Fatal("site was not removed from OhDear")
 52 | 		}
 53 | 
 54 | 		if e.Response.StatusCode() != 404 {
 55 | 			t.Fatal("site was not removed from Oh Dear")
 56 | 		}
 57 | 	})
 58 | 
 59 | 	uptime, enabled := getCheckInfo(create)
 60 | 
 61 | 	assert.Equal(t, "https://example.com", create.URL)
 62 | 	assert.ElementsMatch(t, []string{"uptime", "performance"}, enabled)
 63 | 
 64 | 	// get the site
 65 | 	site, err := client.GetSite(create.ID)
 66 | 	require.NoError(t, err)
 67 | 	assert.Equal(t, create, site)
 68 | 
 69 | 	// disable the uptime check
 70 | 	err = client.DisableCheck(uptime)
 71 | 	require.NoError(t, err)
 72 | 	update, err := client.GetSite(site.ID)
 73 | 	require.NoError(t, err)
 74 | 	_, enabled = getCheckInfo(update)
 75 | 	assert.Empty(t, enabled)
 76 | 
 77 | 	// enable the uptime check
 78 | 	err = client.EnableCheck(uptime)
 79 | 	require.NoError(t, err)
 80 | 	update, err = client.GetSite(site.ID)
 81 | 	require.NoError(t, err)
 82 | 	_, enabled = getCheckInfo(update)
 83 | 	assert.ElementsMatch(t, []string{"uptime", "performance"}, enabled)
 84 | 
 85 | 	// delete the site
 86 | 	err = client.RemoveSite(site.ID)
 87 | 	require.NoError(t, err)
 88 | 
 89 | 	// verify it was deleted (wait because sometimes it takes the api a second to update)
 90 | 	time.Sleep(5 * time.Second)
 91 | 	removed, err := client.GetSite(site.ID)
 92 | 	assert.Nil(t, removed)
 93 | 
 94 | 	var e *Error
 95 | 	require.ErrorAs(t, err, &e)
 96 | 	assert.Equal(t, 404, e.Response.StatusCode())
 97 | }
 98 | 
 99 | func TestSetUserAgent(t *testing.T) {
100 | 	_, reset := mocklog()
101 | 	t.Cleanup(reset)
102 | 	t.Cleanup(httpmock.DeactivateAndReset)
103 | 
104 | 	httpmock.RegisterResponder("GET", "https://ohdear.test/ping",
105 | 		func(req *http.Request) (*http.Response, error) {
106 | 			assert.Equal(t, "application/json", req.Header.Get("Accept"))
107 | 			assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
108 | 			assert.Equal(t, "user-agent/test", req.Header.Get("User-Agent"))
109 | 
110 | 			return httpmock.NewStringResponse(200, ""), nil
111 | 		})
112 | 
113 | 	client := NewClient("https://ohdear.test", "")
114 | 	httpmock.ActivateNonDefault(client.GetClient())
115 | 
116 | 	client.SetUserAgent("user-agent/test")
117 | 	_, err := client.R().Get("/ping")
118 | 	require.NoError(t, err)
119 | }
120 | 
121 | func getCheckInfo(s *Site) (int, []string) {
122 | 	uptime := 0
123 | 	enabled := []string{}
124 | 	for _, check := range s.Checks {
125 | 		if check.Enabled {
126 | 			enabled = append(enabled, check.Type)
127 | 		}
128 | 		if check.Type == "uptime" {
129 | 			uptime = check.ID
130 | 		}
131 | 	}
132 | 
133 | 	return uptime, enabled
134 | }
135 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/articulate/terraform-provider-ohdear
 2 | 
 3 | go 1.24.0
 4 | 
 5 | require (
 6 | 	github.com/go-resty/resty/v2 v2.16.5
 7 | 	github.com/hashicorp/terraform-plugin-docs v0.23.0
 8 | 	github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1
 9 | 	github.com/jarcoal/httpmock v1.4.1
10 | 	github.com/stretchr/testify v1.11.1
11 | )
12 | 
13 | require (
14 | 	github.com/BurntSushi/toml v1.2.1 // indirect
15 | 	github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
16 | 	github.com/Masterminds/goutils v1.1.1 // indirect
17 | 	github.com/Masterminds/semver/v3 v3.2.0 // indirect
18 | 	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
19 | 	github.com/ProtonMail/go-crypto v1.1.6 // indirect
20 | 	github.com/agext/levenshtein v1.2.2 // indirect
21 | 	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
22 | 	github.com/armon/go-radix v1.0.0 // indirect
23 | 	github.com/bgentry/speakeasy v0.1.0 // indirect
24 | 	github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
25 | 	github.com/cloudflare/circl v1.6.1 // indirect
26 | 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
27 | 	github.com/fatih/color v1.16.0 // indirect
28 | 	github.com/golang/protobuf v1.5.4 // indirect
29 | 	github.com/google/go-cmp v0.7.0 // indirect
30 | 	github.com/google/uuid v1.6.0 // indirect
31 | 	github.com/hashicorp/cli v1.1.7 // indirect
32 | 	github.com/hashicorp/errwrap v1.1.0 // indirect
33 | 	github.com/hashicorp/go-checkpoint v0.5.0 // indirect
34 | 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
35 | 	github.com/hashicorp/go-cty v1.5.0 // indirect
36 | 	github.com/hashicorp/go-hclog v1.6.3 // indirect
37 | 	github.com/hashicorp/go-multierror v1.1.1 // indirect
38 | 	github.com/hashicorp/go-plugin v1.7.0 // indirect
39 | 	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
40 | 	github.com/hashicorp/go-uuid v1.0.3 // indirect
41 | 	github.com/hashicorp/go-version v1.7.0 // indirect
42 | 	github.com/hashicorp/hc-install v0.9.2 // indirect
43 | 	github.com/hashicorp/hcl/v2 v2.24.0 // indirect
44 | 	github.com/hashicorp/logutils v1.0.0 // indirect
45 | 	github.com/hashicorp/terraform-exec v0.23.1 // indirect
46 | 	github.com/hashicorp/terraform-json v0.27.1 // indirect
47 | 	github.com/hashicorp/terraform-plugin-go v0.29.0 // indirect
48 | 	github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
49 | 	github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
50 | 	github.com/hashicorp/terraform-svchost v0.1.1 // indirect
51 | 	github.com/hashicorp/yamux v0.1.2 // indirect
52 | 	github.com/huandu/xstrings v1.3.3 // indirect
53 | 	github.com/imdario/mergo v0.3.15 // indirect
54 | 	github.com/mattn/go-colorable v0.1.14 // indirect
55 | 	github.com/mattn/go-isatty v0.0.20 // indirect
56 | 	github.com/mattn/go-runewidth v0.0.9 // indirect
57 | 	github.com/mitchellh/copystructure v1.2.0 // indirect
58 | 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
59 | 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
60 | 	github.com/mitchellh/mapstructure v1.5.0 // indirect
61 | 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
62 | 	github.com/oklog/run v1.1.0 // indirect
63 | 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
64 | 	github.com/posener/complete v1.2.3 // indirect
65 | 	github.com/shopspring/decimal v1.3.1 // indirect
66 | 	github.com/spf13/cast v1.5.0 // indirect
67 | 	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
68 | 	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
69 | 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
70 | 	github.com/yuin/goldmark v1.7.7 // indirect
71 | 	github.com/yuin/goldmark-meta v1.1.0 // indirect
72 | 	github.com/zclconf/go-cty v1.17.0 // indirect
73 | 	go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
74 | 	golang.org/x/crypto v0.42.0 // indirect
75 | 	golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
76 | 	golang.org/x/mod v0.27.0 // indirect
77 | 	golang.org/x/net v0.43.0 // indirect
78 | 	golang.org/x/sync v0.17.0 // indirect
79 | 	golang.org/x/sys v0.36.0 // indirect
80 | 	golang.org/x/text v0.29.0 // indirect
81 | 	golang.org/x/tools v0.36.0 // indirect
82 | 	google.golang.org/appengine v1.6.8 // indirect
83 | 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
84 | 	google.golang.org/grpc v1.75.1 // indirect
85 | 	google.golang.org/protobuf v1.36.9 // indirect
86 | 	gopkg.in/yaml.v2 v2.3.0 // indirect
87 | 	gopkg.in/yaml.v3 v3.0.1 // indirect
88 | )
89 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
 1 | # Changelog
 2 | 
 3 | ## [3.0.2](https://github.com/articulate/terraform-provider-ohdear/compare/v3.0.1...v3.0.2) (2025-10-08)
 4 | 
 5 | 
 6 | ### Dependency Updates
 7 | 
 8 | * bump the terraform group with 2 updates ([#171](https://github.com/articulate/terraform-provider-ohdear/issues/171)) ([f9944fd](https://github.com/articulate/terraform-provider-ohdear/commit/f9944fd54530f0266a275b4bada072b755f32ba7))
 9 | 
10 | ## [3.0.1](https://github.com/articulate/terraform-provider-ohdear/compare/v3.0.0...v3.0.1) (2025-09-02)
11 | 
12 | 
13 | ### Dependency Updates
14 | 
15 | * bump the terraform group across 1 directory with 2 updates ([#162](https://github.com/articulate/terraform-provider-ohdear/issues/162)) ([c6aa173](https://github.com/articulate/terraform-provider-ohdear/commit/c6aa173b6d6432a287a844c39a218088b5db2c2c))
16 | * bump the test group with 2 updates ([#167](https://github.com/articulate/terraform-provider-ohdear/issues/167)) ([601268f](https://github.com/articulate/terraform-provider-ohdear/commit/601268f35d7114037bdc948300f0baa59915fa8a))
17 | 
18 | ## [3.0.0](https://github.com/articulate/terraform-provider-ohdear/compare/v2.3.0...v3.0.0) (2025-08-28)
19 | 
20 | 
21 | ### ⚠ BREAKING CHANGES
22 | 
23 | * deprecate ohdear_site in favor of new ohdear_monitor ([#165](https://github.com/articulate/terraform-provider-ohdear/issues/165))
24 | 
25 | ### Features
26 | 
27 | * deprecate ohdear_site in favor of new ohdear_monitor ([#165](https://github.com/articulate/terraform-provider-ohdear/issues/165)) ([935bdc0](https://github.com/articulate/terraform-provider-ohdear/commit/935bdc0b1f351de92156edc971e3fb5407cb5794))
28 | 
29 | ## [2.3.0](https://github.com/articulate/terraform-provider-ohdear/compare/v2.2.4...v2.3.0) (2025-05-08)
30 | 
31 | 
32 | ### Features
33 | 
34 | * upgrade golangci-lint to v2 ([#155](https://github.com/articulate/terraform-provider-ohdear/issues/155)) ([f8c71e7](https://github.com/articulate/terraform-provider-ohdear/commit/f8c71e7465bd6e7c8b33ae80e02af2c38b40d8c2))
35 | 
36 | 
37 | ### Dependency Updates
38 | 
39 | * bump github.com/go-resty/resty/v2 from 2.16.2 to 2.16.5 ([#150](https://github.com/articulate/terraform-provider-ohdear/issues/150)) ([b2abd2b](https://github.com/articulate/terraform-provider-ohdear/commit/b2abd2b8a5f752e95ed5a6ad8191f0bebfa2182a))
40 | * bump github.com/jarcoal/httpmock from 1.3.1 to 1.4.0 in the test group ([#157](https://github.com/articulate/terraform-provider-ohdear/issues/157)) ([9486df3](https://github.com/articulate/terraform-provider-ohdear/commit/9486df35bf2eea4e504ed04950c610ae1d90dc1b))
41 | * bump the terraform group with 2 updates ([#152](https://github.com/articulate/terraform-provider-ohdear/issues/152)) ([9555346](https://github.com/articulate/terraform-provider-ohdear/commit/9555346de8d58320f75fd5440255edad880d3a41))
42 | 
43 | ## [2.2.4](https://github.com/articulate/terraform-provider-ohdear/compare/v2.2.3...v2.2.4) (2024-12-04)
44 | 
45 | 
46 | ### Dependency Updates
47 | 
48 | * bump github.com/go-resty/resty/v2 from 2.15.3 to 2.16.2 ([#145](https://github.com/articulate/terraform-provider-ohdear/issues/145)) ([73f4759](https://github.com/articulate/terraform-provider-ohdear/commit/73f4759f6972662f1ce9765df2e55dcf4cb15f0e))
49 | * bump github.com/hashicorp/terraform-plugin-docs from 0.19.4 to 0.20.1 in the terraform group ([#143](https://github.com/articulate/terraform-provider-ohdear/issues/143)) ([2afc59e](https://github.com/articulate/terraform-provider-ohdear/commit/2afc59ee8d35d0be52a4e747b4cc0393cb63e324))
50 | * bump github.com/stretchr/testify from 1.9.0 to 1.10.0 in the test group ([#144](https://github.com/articulate/terraform-provider-ohdear/issues/144)) ([e3a37fe](https://github.com/articulate/terraform-provider-ohdear/commit/e3a37fee61efae433d33aaadee54b8e7ec2f86bd))
51 | 
52 | ## [2.2.3](https://github.com/articulate/terraform-provider-ohdear/compare/v2.2.2...v2.2.3) (2024-11-14)
53 | 
54 | 
55 | ### Dependency Updates
56 | 
57 | * bump github.com/hashicorp/terraform-plugin-sdk/v2 from 2.34.0 to 2.35.0 in the terraform group ([#140](https://github.com/articulate/terraform-provider-ohdear/issues/140)) ([c287ddd](https://github.com/articulate/terraform-provider-ohdear/commit/c287dddfda55c98c0907c819579a6d85ca640634))
58 | 
59 | ## [2.2.2](https://github.com/articulate/terraform-provider-ohdear/compare/v2.2.1...v2.2.2) (2024-10-25)
60 | 
61 | 
62 | ### Continuous Integration
63 | 
64 | * **release:** create goreleaser workflow that runs on tags ([#137](https://github.com/articulate/terraform-provider-ohdear/issues/137)) ([38be960](https://github.com/articulate/terraform-provider-ohdear/commit/38be96010719ad3e1a220e6456171ce86d8798ae))
65 | 
66 | ## [2.2.1](https://github.com/articulate/terraform-provider-ohdear/compare/v2.2.0...v2.2.1) (2024-10-25)
67 | 
68 | 
69 | ### Continuous Integration
70 | 
71 | * **release:** create tag for goreleaser ([#135](https://github.com/articulate/terraform-provider-ohdear/issues/135)) ([66c4937](https://github.com/articulate/terraform-provider-ohdear/commit/66c493735fa86d71be5e23f7c73d1798adbb5579))
72 | 
73 | ## [2.2.0](https://github.com/articulate/terraform-provider-ohdear/compare/v2.1.13...v2.2.0) (2024-10-25)
74 | 
75 | 
76 | ### Features
77 | 
78 | * **golang:** bumps to version 1.23 ([f8b0244](https://github.com/articulate/terraform-provider-ohdear/commit/f8b0244ba83492e74ff2b4bb232cacf0f8bcbfdc))
79 | 
80 | 
81 | ### Dependency Updates
82 | 
83 | * bump github.com/go-resty/resty/v2 from 2.13.1 to 2.15.3 ([#131](https://github.com/articulate/terraform-provider-ohdear/issues/131)) ([2d71685](https://github.com/articulate/terraform-provider-ohdear/commit/2d716850f8742e7bf2205f99e54633c96bfb97c0))
84 | 


--------------------------------------------------------------------------------
/internal/provider/resource_monitor.go:
--------------------------------------------------------------------------------
  1 | package provider
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"log"
  7 | 	"strconv"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 11 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 12 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 13 | 
 14 | 	"github.com/articulate/terraform-provider-ohdear/pkg/ohdear"
 15 | )
 16 | 
 17 | func resourceOhdearMonitor() *schema.Resource {
 18 | 	return &schema.Resource{
 19 | 		Description:   "`ohdear_monitor` manages a monitor in Oh Dear.",
 20 | 		CreateContext: resourceOhdearMonitorCreate,
 21 | 		ReadContext:   resourceOhdearMonitorRead,
 22 | 		DeleteContext: resourceOhdearMonitorDelete,
 23 | 		UpdateContext: resourceOhdearMonitorUpdate,
 24 | 		Schema: map[string]*schema.Schema{
 25 | 			"url": {
 26 | 				Type:         schema.TypeString,
 27 | 				Required:     true,
 28 | 				ForceNew:     true,
 29 | 				Description:  "URL of the monitor to be checked.",
 30 | 				ValidateFunc: validation.IsURLWithHTTPorHTTPS,
 31 | 			},
 32 | 			"team_id": {
 33 | 				Type:        schema.TypeInt,
 34 | 				Optional:    true,
 35 | 				Computed:    true,
 36 | 				ForceNew:    true,
 37 | 				Description: "ID of the team for this monitor. If not set, will use `team_id` configured in provider.",
 38 | 			},
 39 | 			"checks": {
 40 | 				Type:        schema.TypeList,
 41 | 				Description: "Set the checks enabled for the monitor. If block is not present, it will enable all checks.",
 42 | 				Optional:    true,
 43 | 				Computed:    true,
 44 | 				MaxItems:    1,
 45 | 				Elem: &schema.Resource{
 46 | 					Schema: map[string]*schema.Schema{
 47 | 						ohdear.UptimeCheck: {
 48 | 							Type:        schema.TypeBool,
 49 | 							Description: "Enable uptime checks.",
 50 | 							Optional:    true,
 51 | 						},
 52 | 						ohdear.BrokenLinksCheck: {
 53 | 							Type:        schema.TypeBool,
 54 | 							Description: "Enable broken link checks.",
 55 | 							Optional:    true,
 56 | 						},
 57 | 						ohdear.CertificateHealthCheck: {
 58 | 							Type:        schema.TypeBool,
 59 | 							Description: "Enable certificate health checks. Requires the url to use https.",
 60 | 							Optional:    true,
 61 | 						},
 62 | 						ohdear.CertificateTransparencyCheck: {
 63 | 							Type:        schema.TypeBool,
 64 | 							Description: "Enable certificate transparency checks. Requires the url to use https.",
 65 | 							Optional:    true,
 66 | 							Deprecated: "This check was removed by OhDear and will be removed " +
 67 | 								"in a future major release.",
 68 | 							DiffSuppressFunc: func(_, _, _ string, _ *schema.ResourceData) bool {
 69 | 								return true
 70 | 							},
 71 | 						},
 72 | 						ohdear.MixedContentCheck: {
 73 | 							Type:        schema.TypeBool,
 74 | 							Description: "Enable mixed content checks.",
 75 | 							Optional:    true,
 76 | 						},
 77 | 						ohdear.PerformanceCheck: {
 78 | 							Type:        schema.TypeBool,
 79 | 							Description: "Enable performance checks.",
 80 | 							Optional:    true,
 81 | 							Deprecated: "This check was merged with the 'uptime' check by OhDear and will be removed " +
 82 | 								"in a future major release.",
 83 | 							DiffSuppressFunc: func(_, _, _ string, _ *schema.ResourceData) bool {
 84 | 								return true
 85 | 							},
 86 | 						},
 87 | 						ohdear.DNSCheck: {
 88 | 							Type:        schema.TypeBool,
 89 | 							Description: "Enable DNS checks.",
 90 | 							Default:     false,
 91 | 							Optional:    true,
 92 | 						},
 93 | 					},
 94 | 				},
 95 | 			},
 96 | 		},
 97 | 		CustomizeDiff: resourceOhdearMonitorDiff,
 98 | 		Importer: &schema.ResourceImporter{
 99 | 			StateContext: schema.ImportStatePassthroughContext,
100 | 		},
101 | 	}
102 | }
103 | 
104 | func getMonitorID(d *schema.ResourceData) (int, error) {
105 | 	id, err := strconv.Atoi(d.Id())
106 | 	if err != nil {
107 | 		return id, fmt.Errorf("corrupted resource ID in terraform state, Oh Dear only supports integer IDs. Err: %w", err)
108 | 	}
109 | 	return id, err
110 | }
111 | 
112 | func resourceOhdearMonitorDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
113 | 	checks := d.Get("checks").([]interface{})
114 | 	if len(checks) == 0 {
115 | 		isHTTPS := strings.HasPrefix(d.Get("url").(string), "https")
116 | 		checks = append(checks, map[string]bool{
117 | 			ohdear.UptimeCheck:            true,
118 | 			ohdear.BrokenLinksCheck:       true,
119 | 			ohdear.CertificateHealthCheck: isHTTPS,
120 | 			ohdear.MixedContentCheck:      isHTTPS,
121 | 			ohdear.DNSCheck:               false, // TODO: turn to true on next major release (breaking change)
122 | 		})
123 | 
124 | 		if err := d.SetNew("checks", checks); err != nil {
125 | 			return err
126 | 		}
127 | 	}
128 | 
129 | 	// set team_id from provider default if not provided
130 | 	if d.Get("team_id") == 0 {
131 | 		return d.SetNew("team_id", meta.(*Config).teamID)
132 | 	}
133 | 
134 | 	return nil
135 | }
136 | 
137 | func resourceOhdearMonitorCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
138 | 	log.Println("[DEBUG] Calling Create lifecycle function for monitor")
139 | 
140 | 	client := meta.(*Config).client
141 | 	monitor, err := client.AddMonitor(d.Get("url").(string), d.Get("team_id").(int), checksWanted(d))
142 | 	if err != nil {
143 | 		return diagErrorf(err, "Could not add monitor to Oh Dear")
144 | 	}
145 | 
146 | 	d.SetId(strconv.Itoa(monitor.ID))
147 | 
148 | 	return resourceOhdearMonitorRead(ctx, d, meta)
149 | }
150 | 
151 | func resourceOhdearMonitorRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
152 | 	log.Printf("[DEBUG] Calling Read lifecycle function for monitor %s\n", d.Id())
153 | 
154 | 	id, err := getMonitorID(d)
155 | 	if err != nil {
156 | 		return diag.FromErr(err)
157 | 	}
158 | 
159 | 	client := meta.(*Config).client
160 | 	monitor, err := client.GetMonitor(id)
161 | 	if err != nil {
162 | 		return diagErrorf(err, "Could not find monitor %d in Oh Dear", id)
163 | 	}
164 | 
165 | 	checks := checkStateMapFromMonitor(monitor)
166 | 	if err := d.Set("checks", []interface{}{checks}); err != nil {
167 | 		return diag.FromErr(err)
168 | 	}
169 | 
170 | 	if err := d.Set("url", monitor.URL); err != nil {
171 | 		return diag.FromErr(err)
172 | 	}
173 | 
174 | 	if err := d.Set("team_id", monitor.TeamID); err != nil {
175 | 		return diag.FromErr(err)
176 | 	}
177 | 
178 | 	return nil
179 | }
180 | 
181 | func resourceOhdearMonitorDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
182 | 	log.Printf("[DEBUG] Calling Delete lifecycle function for monitor %s\n", d.Id())
183 | 
184 | 	id, err := getMonitorID(d)
185 | 	if err != nil {
186 | 		return diag.FromErr(err)
187 | 	}
188 | 
189 | 	client := meta.(*Config).client
190 | 	if err = client.RemoveMonitor(id); err != nil {
191 | 		return diagErrorf(err, "Could not remove monitor %d from Oh Dear", id)
192 | 	}
193 | 
194 | 	return nil
195 | }
196 | 
197 | func resourceOhdearMonitorUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
198 | 	log.Printf("[DEBUG] Calling Update lifecycle function for monitor %s\n", d.Id())
199 | 
200 | 	id, err := getMonitorID(d)
201 | 	if err != nil {
202 | 		return diag.FromErr(err)
203 | 	}
204 | 
205 | 	client := meta.(*Config).client
206 | 	monitor, err := client.GetMonitor(id)
207 | 	if err != nil {
208 | 		return diagErrorf(err, "Could not find monitor in Oh Dear")
209 | 	}
210 | 
211 | 	// Sync downstream checks with config
212 | 	checksWanted := checksWanted(d)
213 | 	for _, check := range monitor.Checks {
214 | 		if check.Enabled {
215 | 			if !contains(checksWanted, check.Type) {
216 | 				if err := client.DisableCheck(check.ID); err != nil {
217 | 					return diagErrorf(err, "Could not remove check to monitor in Oh Dear")
218 | 				}
219 | 			}
220 | 		} else {
221 | 			if contains(checksWanted, check.Type) {
222 | 				if err := client.EnableCheck(check.ID); err != nil {
223 | 					return diagErrorf(err, "Could not add check to monitor in Oh Dear")
224 | 				}
225 | 			}
226 | 		}
227 | 	}
228 | 
229 | 	return resourceOhdearMonitorRead(ctx, d, meta)
230 | }
231 | 
232 | func checkStateMapFromMonitor(monitor *ohdear.Monitor) map[string]bool {
233 | 	result := make(map[string]bool)
234 | 	for _, check := range monitor.Checks {
235 | 		if contains(ohdear.AllChecks, check.Type) {
236 | 			result[check.Type] = check.Enabled
237 | 		}
238 | 	}
239 | 
240 | 	return result
241 | }
242 | 


--------------------------------------------------------------------------------
/internal/provider/resource_site.go:
--------------------------------------------------------------------------------
  1 | package provider
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"log"
  7 | 	"strconv"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 11 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 12 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 13 | 
 14 | 	"github.com/articulate/terraform-provider-ohdear/pkg/ohdear"
 15 | )
 16 | 
 17 | func resourceOhdearSite() *schema.Resource {
 18 | 	return &schema.Resource{
 19 | 		Description: "`ohdear_site` manages a site in Oh Dear.",
 20 | 		DeprecationMessage: "`ohdear_site` is deprecated and will be removed in a future major release. " +
 21 | 			"Please use `ohdear_monitor` instead, which now supports all site functionality.",
 22 | 
 23 | 		CreateContext: resourceOhdearSiteCreate,
 24 | 		ReadContext:   resourceOhdearSiteRead,
 25 | 		DeleteContext: resourceOhdearSiteDelete,
 26 | 		UpdateContext: resourceOhdearSiteUpdate,
 27 | 		Schema: map[string]*schema.Schema{
 28 | 			"url": {
 29 | 				Type:         schema.TypeString,
 30 | 				Required:     true,
 31 | 				ForceNew:     true,
 32 | 				Description:  "URL of the site to be checked.",
 33 | 				ValidateFunc: validation.IsURLWithHTTPorHTTPS,
 34 | 			},
 35 | 			"team_id": {
 36 | 				Type:        schema.TypeInt,
 37 | 				Optional:    true,
 38 | 				Computed:    true,
 39 | 				ForceNew:    true,
 40 | 				Description: "ID of the team for this site. If not set, will use `team_id` configured in provider.",
 41 | 			},
 42 | 			"checks": {
 43 | 				Type:        schema.TypeList,
 44 | 				Description: "Set the checks enabled for the site. If block is not present, it will enable all checks.",
 45 | 				Optional:    true,
 46 | 				Computed:    true,
 47 | 				MaxItems:    1,
 48 | 				Elem: &schema.Resource{
 49 | 					Schema: map[string]*schema.Schema{
 50 | 						ohdear.UptimeCheck: {
 51 | 							Type:        schema.TypeBool,
 52 | 							Description: "Enable uptime checks.",
 53 | 							Optional:    true,
 54 | 						},
 55 | 						ohdear.BrokenLinksCheck: {
 56 | 							Type:        schema.TypeBool,
 57 | 							Description: "Enable broken link checks.",
 58 | 							Optional:    true,
 59 | 						},
 60 | 						ohdear.CertificateHealthCheck: {
 61 | 							Type:        schema.TypeBool,
 62 | 							Description: "Enable certificate health checks. Requires the url to use https.",
 63 | 							Optional:    true,
 64 | 						},
 65 | 						ohdear.CertificateTransparencyCheck: {
 66 | 							Type:        schema.TypeBool,
 67 | 							Description: "Enable certificate transparency checks. Requires the url to use https.",
 68 | 							Optional:    true,
 69 | 							Deprecated: "This check was removed by OhDear and will be removed " +
 70 | 								"in a future major release.",
 71 | 							DiffSuppressFunc: func(_, _, _ string, _ *schema.ResourceData) bool {
 72 | 								return true
 73 | 							},
 74 | 						},
 75 | 						ohdear.MixedContentCheck: {
 76 | 							Type:        schema.TypeBool,
 77 | 							Description: "Enable mixed content checks.",
 78 | 							Optional:    true,
 79 | 						},
 80 | 						ohdear.PerformanceCheck: {
 81 | 							Type:        schema.TypeBool,
 82 | 							Description: "Enable performance checks.",
 83 | 							Optional:    true,
 84 | 							Deprecated: "This check was merged with the 'uptime' check by OhDear and will be removed " +
 85 | 								"in a future major release.",
 86 | 							DiffSuppressFunc: func(_, _, _ string, _ *schema.ResourceData) bool {
 87 | 								return true
 88 | 							},
 89 | 						},
 90 | 						ohdear.DNSCheck: {
 91 | 							Type:        schema.TypeBool,
 92 | 							Description: "Enable DNS checks.",
 93 | 							Default:     false,
 94 | 							Optional:    true,
 95 | 						},
 96 | 					},
 97 | 				},
 98 | 			},
 99 | 		},
100 | 		CustomizeDiff: resourceOhdearSiteDiff,
101 | 		Importer: &schema.ResourceImporter{
102 | 			StateContext: schema.ImportStatePassthroughContext,
103 | 		},
104 | 	}
105 | }
106 | 
107 | func getSiteID(d *schema.ResourceData) (int, error) {
108 | 	id, err := strconv.Atoi(d.Id())
109 | 	if err != nil {
110 | 		return id, fmt.Errorf("corrupted resource ID in terraform state, Oh Dear only supports integer IDs. Err: %w", err)
111 | 	}
112 | 	return id, err
113 | }
114 | 
115 | func resourceOhdearSiteDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
116 | 	checks := d.Get("checks").([]interface{})
117 | 	if len(checks) == 0 {
118 | 		isHTTPS := strings.HasPrefix(d.Get("url").(string), "https")
119 | 		checks = append(checks, map[string]bool{
120 | 			ohdear.UptimeCheck:            true,
121 | 			ohdear.BrokenLinksCheck:       true,
122 | 			ohdear.CertificateHealthCheck: isHTTPS,
123 | 			ohdear.MixedContentCheck:      isHTTPS,
124 | 			ohdear.DNSCheck:               false, // TODO: turn to true on next major release (breaking change)
125 | 		})
126 | 
127 | 		if err := d.SetNew("checks", checks); err != nil {
128 | 			return err
129 | 		}
130 | 	}
131 | 
132 | 	// set team_id from provider default if not provided
133 | 	if d.Get("team_id") == 0 {
134 | 		return d.SetNew("team_id", meta.(*Config).teamID)
135 | 	}
136 | 
137 | 	return nil
138 | }
139 | 
140 | func resourceOhdearSiteCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
141 | 	log.Println("[DEBUG] Calling Create lifecycle function for site")
142 | 
143 | 	client := meta.(*Config).client
144 | 	site, err := client.AddSite(d.Get("url").(string), d.Get("team_id").(int), checksWanted(d))
145 | 	if err != nil {
146 | 		return diagErrorf(err, "Could not add site to Oh Dear")
147 | 	}
148 | 
149 | 	d.SetId(strconv.Itoa(site.ID))
150 | 
151 | 	return resourceOhdearSiteRead(ctx, d, meta)
152 | }
153 | 
154 | func resourceOhdearSiteRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
155 | 	log.Printf("[DEBUG] Calling Read lifecycle function for site %s\n", d.Id())
156 | 
157 | 	id, err := getSiteID(d)
158 | 	if err != nil {
159 | 		return diag.FromErr(err)
160 | 	}
161 | 
162 | 	client := meta.(*Config).client
163 | 	site, err := client.GetSite(id)
164 | 	if err != nil {
165 | 		return diagErrorf(err, "Could not find site %d in Oh Dear", id)
166 | 	}
167 | 
168 | 	checks := checkStateMapFromSite(site)
169 | 	if err := d.Set("checks", []interface{}{checks}); err != nil {
170 | 		return diag.FromErr(err)
171 | 	}
172 | 
173 | 	if err := d.Set("url", site.URL); err != nil {
174 | 		return diag.FromErr(err)
175 | 	}
176 | 
177 | 	if err := d.Set("team_id", site.TeamID); err != nil {
178 | 		return diag.FromErr(err)
179 | 	}
180 | 
181 | 	return nil
182 | }
183 | 
184 | func resourceOhdearSiteDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
185 | 	log.Printf("[DEBUG] Calling Delete lifecycle function for site %s\n", d.Id())
186 | 
187 | 	id, err := getSiteID(d)
188 | 	if err != nil {
189 | 		return diag.FromErr(err)
190 | 	}
191 | 
192 | 	client := meta.(*Config).client
193 | 	if err = client.RemoveSite(id); err != nil {
194 | 		return diagErrorf(err, "Could not remove site %d from Oh Dear", id)
195 | 	}
196 | 
197 | 	return nil
198 | }
199 | 
200 | func resourceOhdearSiteUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
201 | 	log.Printf("[DEBUG] Calling Update lifecycle function for site %s\n", d.Id())
202 | 
203 | 	id, err := getSiteID(d)
204 | 	if err != nil {
205 | 		return diag.FromErr(err)
206 | 	}
207 | 
208 | 	client := meta.(*Config).client
209 | 	site, err := client.GetSite(id)
210 | 	if err != nil {
211 | 		return diagErrorf(err, "Could not find site in Oh Dear")
212 | 	}
213 | 
214 | 	// Sync downstream checks with config
215 | 	checksWanted := checksWanted(d)
216 | 	for _, check := range site.Checks {
217 | 		if check.Enabled {
218 | 			if !contains(checksWanted, check.Type) {
219 | 				if err := client.DisableCheck(check.ID); err != nil {
220 | 					return diagErrorf(err, "Could not remove check to site in Oh Dear")
221 | 				}
222 | 			}
223 | 		} else {
224 | 			if contains(checksWanted, check.Type) {
225 | 				if err := client.EnableCheck(check.ID); err != nil {
226 | 					return diagErrorf(err, "Could not add check to site in Oh Dear")
227 | 				}
228 | 			}
229 | 		}
230 | 	}
231 | 
232 | 	return resourceOhdearSiteRead(ctx, d, meta)
233 | }
234 | 
235 | func checkStateMapFromSite(site *ohdear.Site) map[string]bool {
236 | 	result := make(map[string]bool)
237 | 	for _, check := range site.Checks {
238 | 		if contains(ohdear.AllChecks, check.Type) {
239 | 			result[check.Type] = check.Enabled
240 | 		}
241 | 	}
242 | 
243 | 	return result
244 | }
245 | 
246 | func checksWanted(d *schema.ResourceData) []string {
247 | 	checks := []string{}
248 | 	schema := d.Get("checks").([]interface{})[0].(map[string]interface{})
249 | 	for check, enabled := range schema {
250 | 		if enabled.(bool) {
251 | 			checks = append(checks, check)
252 | 		}
253 | 	}
254 | 
255 | 	return checks
256 | }
257 | 


--------------------------------------------------------------------------------
/internal/provider/resource_monitor_test.go:
--------------------------------------------------------------------------------
  1 | package provider
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"fmt"
  6 | 	"strconv"
  7 | 	"strings"
  8 | 	"testing"
  9 | 	"time"
 10 | 
 11 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
 12 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 13 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
 14 | 
 15 | 	"github.com/articulate/terraform-provider-ohdear/pkg/ohdear"
 16 | )
 17 | 
 18 | func TestAccOhdearMonitor(t *testing.T) {
 19 | 	name := acctest.RandomWithPrefix("tf-acc-test")
 20 | 	url := "https://example.com/" + name
 21 | 	resourceName := "ohdear_monitor." + name
 22 | 	updatedURL := url + "/new"
 23 | 
 24 | 	resource.Test(t, resource.TestCase{
 25 | 		PreCheck:          func() { testAccPreCheck(t) },
 26 | 		IDRefreshName:     resourceName,
 27 | 		ProviderFactories: testAccProviderFactories,
 28 | 		CheckDestroy:      testAccCheckMonitorDestroy,
 29 | 		Steps: []resource.TestStep{
 30 | 			{
 31 | 				Config: testAccOhdearMonitorConfigBasic(name, url),
 32 | 				Check: resource.ComposeAggregateTestCheckFunc(
 33 | 					testAccEnsureMonitorExists(resourceName),
 34 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
 35 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
 36 | 					testAccEnsureChecksEnabled(resourceName, []string{
 37 | 						"uptime", "broken_links", "certificate_health",
 38 | 						"mixed_content", "performance",
 39 | 					}),
 40 | 					testAccEnsureChecksDisabled(resourceName, []string{"dns"}),
 41 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
 42 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "true"),
 43 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.certificate_health", "true"),
 44 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.mixed_content", "true"),
 45 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
 46 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.dns", "false"),
 47 | 				),
 48 | 			},
 49 | 			{
 50 | 				ResourceName:      resourceName,
 51 | 				ImportState:       true,
 52 | 				ImportStateVerify: true,
 53 | 			},
 54 | 			{
 55 | 				Config: testAccOhdearMonitorConfigBasic(name, updatedURL),
 56 | 				Check: resource.ComposeAggregateTestCheckFunc(
 57 | 					testAccEnsureMonitorExists(resourceName),
 58 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
 59 | 					resource.TestCheckResourceAttr(resourceName, "url", updatedURL),
 60 | 				),
 61 | 			},
 62 | 		},
 63 | 	})
 64 | }
 65 | 
 66 | func TestAccOhdearMonitor_EnableDisableChecks(t *testing.T) {
 67 | 	name := acctest.RandomWithPrefix("tf-acc-test")
 68 | 	url := "https://example.com/" + name
 69 | 	resourceName := "ohdear_monitor." + name
 70 | 
 71 | 	resource.Test(t, resource.TestCase{
 72 | 		PreCheck:          func() { testAccPreCheck(t) },
 73 | 		IDRefreshName:     resourceName,
 74 | 		ProviderFactories: testAccProviderFactories,
 75 | 		CheckDestroy:      testAccCheckMonitorDestroy,
 76 | 		Steps: []resource.TestStep{
 77 | 			{
 78 | 				Config: testAccOhdearMonitorConfigChecks(name, url, map[string]bool{"uptime": true, "broken_links": true}),
 79 | 				Check: resource.ComposeAggregateTestCheckFunc(
 80 | 					testAccEnsureMonitorExists(resourceName),
 81 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
 82 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
 83 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
 84 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "true"),
 85 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.certificate_health", "false"),
 86 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.mixed_content", "false"),
 87 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
 88 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.dns", "false"),
 89 | 					testAccEnsureChecksEnabled(resourceName, []string{"uptime", "broken_links"}),
 90 | 					testAccEnsureChecksDisabled(resourceName, []string{"mixed_content"}),
 91 | 				),
 92 | 			},
 93 | 			{
 94 | 				Config: testAccOhdearMonitorConfigChecks(name, url, map[string]bool{"uptime": true}),
 95 | 				Check: resource.ComposeAggregateTestCheckFunc(
 96 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
 97 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
 98 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "false"),
 99 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
100 | 					testAccEnsureChecksEnabled(resourceName, []string{"uptime"}),
101 | 					testAccEnsureChecksDisabled(resourceName, []string{"broken_links"}),
102 | 				),
103 | 			},
104 | 			{
105 | 				Config: testAccOhdearMonitorConfigChecks(name, url, map[string]bool{"uptime": false}),
106 | 				Check: resource.ComposeAggregateTestCheckFunc(
107 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
108 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "false"),
109 | 					testAccEnsureChecksDisabled(resourceName, []string{"uptime", "broken_links"}),
110 | 				),
111 | 			},
112 | 		},
113 | 	})
114 | }
115 | 
116 | func TestAccOhdearMonitor_TeamID(t *testing.T) {
117 | 	name := acctest.RandomWithPrefix("tf-acc-test")
118 | 	url := "https://example.com/" + name
119 | 	resourceName := "ohdear_monitor." + name
120 | 
121 | 	resource.Test(t, resource.TestCase{
122 | 		PreCheck:          func() { testAccPreCheck(t) },
123 | 		IDRefreshName:     resourceName,
124 | 		ProviderFactories: testAccProviderFactories,
125 | 		CheckDestroy:      testAccCheckMonitorDestroy,
126 | 		Steps: []resource.TestStep{
127 | 			{
128 | 				Config: testAccOhdearMonitorConfigBasic(name, url),
129 | 				Check: resource.ComposeAggregateTestCheckFunc(
130 | 					testAccEnsureMonitorExists(resourceName),
131 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
132 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
133 | 				),
134 | 			},
135 | 			{
136 | 				Config:             testAccOhdearMonitorConfigTeamID(name, url, "1"),
137 | 				PlanOnly:           true,
138 | 				ExpectNonEmptyPlan: true,
139 | 				Check: resource.ComposeAggregateTestCheckFunc(
140 | 					resource.TestCheckResourceAttr(resourceName, "team_id", "1"),
141 | 				),
142 | 			},
143 | 		},
144 | 	})
145 | }
146 | 
147 | func TestAccOhdearMonitor_HTTPDefaults(t *testing.T) {
148 | 	name := acctest.RandomWithPrefix("tf-acc-test")
149 | 	url := "http://example.com/" + name
150 | 	resourceName := "ohdear_monitor." + name
151 | 
152 | 	resource.Test(t, resource.TestCase{
153 | 		PreCheck:          func() { testAccPreCheck(t) },
154 | 		IDRefreshName:     resourceName,
155 | 		ProviderFactories: testAccProviderFactories,
156 | 		CheckDestroy:      testAccCheckMonitorDestroy,
157 | 		Steps: []resource.TestStep{
158 | 			{
159 | 				Config:             testAccOhdearMonitorConfigBasic(name, url),
160 | 				PlanOnly:           true,
161 | 				ExpectNonEmptyPlan: true,
162 | 				Check: resource.ComposeAggregateTestCheckFunc(
163 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
164 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "true"),
165 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.certificate_health", "false"),
166 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.mixed_content", "false"),
167 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
168 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.dns", "false"),
169 | 				),
170 | 			},
171 | 		},
172 | 	})
173 | }
174 | 
175 | // Checks
176 | 
177 | func doesMonitorExist(strID string) (bool, error) {
178 | 	client := testAccProvider.Meta().(*Config).client
179 | 	id, _ := strconv.Atoi(strID)
180 | 	if _, err := client.GetMonitor(id); err != nil {
181 | 		var e *ohdear.Error
182 | 		if errors.As(err, &e) && e.Response.StatusCode() == 404 {
183 | 			return false, nil
184 | 		}
185 | 
186 | 		return false, err
187 | 	}
188 | 
189 | 	return true, nil
190 | }
191 | 
192 | func testAccCheckMonitorDestroy(s *terraform.State) error {
193 | 	for _, rs := range s.RootModule().Resources {
194 | 		if rs.Type != "ohdear_monitor" {
195 | 			continue
196 | 		}
197 | 
198 | 		// give the API time to update
199 | 		time.Sleep(5 * time.Second)
200 | 
201 | 		exists, err := doesMonitorExist(rs.Primary.ID)
202 | 		if err != nil {
203 | 			return err
204 | 		}
205 | 
206 | 		if exists {
207 | 			return fmt.Errorf("monitor still exists in Oh Dear: %s", rs.Primary.ID)
208 | 		}
209 | 	}
210 | 	return nil
211 | }
212 | 
213 | func testAccEnsureMonitorExists(name string) resource.TestCheckFunc {
214 | 	return func(s *terraform.State) error {
215 | 		rs, ok := s.RootModule().Resources[name]
216 | 		if !ok {
217 | 			return fmt.Errorf("resource not found: %s", name)
218 | 		}
219 | 
220 | 		exists, err := doesMonitorExist(rs.Primary.ID)
221 | 		if err != nil {
222 | 			return err
223 | 		}
224 | 
225 | 		if !exists {
226 | 			return fmt.Errorf("resource not found: %s", name)
227 | 		}
228 | 
229 | 		return nil
230 | 	}
231 | }
232 | 
233 | // Configs
234 | 
235 | func testAccOhdearMonitorConfigBasic(name, url string) string {
236 | 	return fmt.Sprintf(`
237 | resource "ohdear_monitor" "%s" {
238 |   url = "%s"
239 | }
240 | `, name, url)
241 | }
242 | 
243 | func testAccOhdearMonitorConfigTeamID(name, url, team string) string {
244 | 	return fmt.Sprintf(`
245 | resource "ohdear_monitor" "%s" {
246 |   team_id = %s
247 |   url     = "%s"
248 | }
249 | `, name, team, url)
250 | }
251 | 
252 | func testAccOhdearMonitorConfigChecks(name, url string, checks map[string]bool) string {
253 | 	block := []string{}
254 | 	for check, enabled := range checks {
255 | 		block = append(block, fmt.Sprintf("%s = %t", check, enabled))
256 | 	}
257 | 
258 | 	return fmt.Sprintf(`
259 | resource "ohdear_monitor" "%s" {
260 |   url = "%s"
261 | 
262 |   checks {
263 |     %s
264 |   }
265 | }
266 | `, name, url, strings.Join(block, "\n    "))
267 | }
268 | 


--------------------------------------------------------------------------------
/internal/provider/resource_site_test.go:
--------------------------------------------------------------------------------
  1 | package provider
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"strconv"
  8 | 	"strings"
  9 | 	"testing"
 10 | 	"time"
 11 | 
 12 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
 13 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 14 | 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
 15 | 
 16 | 	"github.com/articulate/terraform-provider-ohdear/pkg/ohdear"
 17 | )
 18 | 
 19 | var teamID string
 20 | 
 21 | func init() {
 22 | 	teamID = os.Getenv("OHDEAR_TEAM_ID")
 23 | }
 24 | 
 25 | func TestAccOhdearSite(t *testing.T) {
 26 | 	name := acctest.RandomWithPrefix("tf-acc-test")
 27 | 	url := "https://example.com/" + name
 28 | 	resourceName := "ohdear_site." + name
 29 | 	updatedURL := url + "/new"
 30 | 
 31 | 	resource.Test(t, resource.TestCase{
 32 | 		PreCheck:          func() { testAccPreCheck(t) },
 33 | 		IDRefreshName:     resourceName,
 34 | 		ProviderFactories: testAccProviderFactories,
 35 | 		CheckDestroy:      testAccCheckSiteDestroy,
 36 | 		Steps: []resource.TestStep{
 37 | 			{
 38 | 				Config: testAccOhdearSiteConfigBasic(name, url),
 39 | 				Check: resource.ComposeAggregateTestCheckFunc(
 40 | 					testAccEnsureSiteExists(resourceName),
 41 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
 42 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
 43 | 					testAccEnsureChecksEnabled(resourceName, []string{
 44 | 						"uptime", "broken_links", "certificate_health",
 45 | 						"mixed_content", "performance",
 46 | 					}),
 47 | 					testAccEnsureChecksDisabled(resourceName, []string{"dns"}),
 48 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
 49 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "true"),
 50 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.certificate_health", "true"),
 51 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.mixed_content", "true"),
 52 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
 53 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.dns", "false"),
 54 | 				),
 55 | 			},
 56 | 			{
 57 | 				ResourceName:      resourceName,
 58 | 				ImportState:       true,
 59 | 				ImportStateVerify: true,
 60 | 			},
 61 | 			{
 62 | 				Config: testAccOhdearSiteConfigBasic(name, updatedURL),
 63 | 				Check: resource.ComposeAggregateTestCheckFunc(
 64 | 					testAccEnsureSiteExists(resourceName),
 65 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
 66 | 					resource.TestCheckResourceAttr(resourceName, "url", updatedURL),
 67 | 				),
 68 | 			},
 69 | 		},
 70 | 	})
 71 | }
 72 | 
 73 | func TestAccOhdearSite_EnableDisableChecks(t *testing.T) {
 74 | 	name := acctest.RandomWithPrefix("tf-acc-test")
 75 | 	url := "https://example.com/" + name
 76 | 	resourceName := "ohdear_site." + name
 77 | 
 78 | 	resource.Test(t, resource.TestCase{
 79 | 		PreCheck:          func() { testAccPreCheck(t) },
 80 | 		IDRefreshName:     resourceName,
 81 | 		ProviderFactories: testAccProviderFactories,
 82 | 		CheckDestroy:      testAccCheckSiteDestroy,
 83 | 		Steps: []resource.TestStep{
 84 | 			{
 85 | 				Config: testAccOhdearSiteConfigChecks(name, url, map[string]bool{"uptime": true, "broken_links": true}),
 86 | 				Check: resource.ComposeAggregateTestCheckFunc(
 87 | 					testAccEnsureSiteExists(resourceName),
 88 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
 89 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
 90 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
 91 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "true"),
 92 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.certificate_health", "false"),
 93 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.mixed_content", "false"),
 94 | 					testAccEnsureChecksEnabled(resourceName, []string{"uptime", "broken_links"}),
 95 | 					testAccEnsureChecksDisabled(resourceName, []string{"mixed_content"}),
 96 | 				),
 97 | 			},
 98 | 			{
 99 | 				Config: testAccOhdearSiteConfigChecks(name, url, map[string]bool{"uptime": true}),
100 | 				Check: resource.ComposeAggregateTestCheckFunc(
101 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
102 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
103 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "false"),
104 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
105 | 					testAccEnsureChecksEnabled(resourceName, []string{"uptime"}),
106 | 					testAccEnsureChecksDisabled(resourceName, []string{"broken_links"}),
107 | 				),
108 | 			},
109 | 			{
110 | 				Config: testAccOhdearSiteConfigChecks(name, url, map[string]bool{"uptime": false}),
111 | 				Check: resource.ComposeAggregateTestCheckFunc(
112 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
113 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "false"),
114 | 					testAccEnsureChecksDisabled(resourceName, []string{"uptime", "broken_links"}),
115 | 				),
116 | 			},
117 | 		},
118 | 	})
119 | }
120 | 
121 | func TestAccOhdearSite_TeamID(t *testing.T) {
122 | 	name := acctest.RandomWithPrefix("tf-acc-test")
123 | 	url := "https://example.com/" + name
124 | 	resourceName := "ohdear_site." + name
125 | 
126 | 	resource.Test(t, resource.TestCase{
127 | 		PreCheck:          func() { testAccPreCheck(t) },
128 | 		IDRefreshName:     resourceName,
129 | 		ProviderFactories: testAccProviderFactories,
130 | 		CheckDestroy:      testAccCheckSiteDestroy,
131 | 		Steps: []resource.TestStep{
132 | 			{
133 | 				Config: testAccOhdearSiteConfigBasic(name, url),
134 | 				Check: resource.ComposeAggregateTestCheckFunc(
135 | 					testAccEnsureSiteExists(resourceName),
136 | 					resource.TestCheckResourceAttr(resourceName, "team_id", teamID),
137 | 					resource.TestCheckResourceAttr(resourceName, "url", url),
138 | 				),
139 | 			},
140 | 			{
141 | 				Config:             testAccOhdearSiteConfigTeamID(name, url, "1"),
142 | 				PlanOnly:           true,
143 | 				ExpectNonEmptyPlan: true,
144 | 				Check: resource.ComposeAggregateTestCheckFunc(
145 | 					resource.TestCheckResourceAttr(resourceName, "team_id", "1"),
146 | 				),
147 | 			},
148 | 		},
149 | 	})
150 | }
151 | 
152 | func TestAccOhdearSite_HTTPDefaults(t *testing.T) {
153 | 	name := acctest.RandomWithPrefix("tf-acc-test")
154 | 	url := "http://example.com/" + name
155 | 	resourceName := "ohdear_site." + name
156 | 
157 | 	resource.Test(t, resource.TestCase{
158 | 		PreCheck:          func() { testAccPreCheck(t) },
159 | 		IDRefreshName:     resourceName,
160 | 		ProviderFactories: testAccProviderFactories,
161 | 		CheckDestroy:      testAccCheckSiteDestroy,
162 | 		Steps: []resource.TestStep{
163 | 			{
164 | 				Config:             testAccOhdearSiteConfigBasic(name, url),
165 | 				PlanOnly:           true,
166 | 				ExpectNonEmptyPlan: true,
167 | 				Check: resource.ComposeAggregateTestCheckFunc(
168 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.uptime", "true"),
169 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.broken_links", "true"),
170 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.certificate_health", "false"),
171 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.mixed_content", "false"),
172 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.performance", "true"),
173 | 					resource.TestCheckResourceAttr(resourceName, "checks.0.dns", "false"),
174 | 				),
175 | 			},
176 | 		},
177 | 	})
178 | }
179 | 
180 | // Checks
181 | 
182 | func doesSiteExists(strID string) (bool, error) {
183 | 	client := testAccProvider.Meta().(*Config).client
184 | 	id, _ := strconv.Atoi(strID)
185 | 	if _, err := client.GetSite(id); err != nil {
186 | 		var e *ohdear.Error
187 | 		if errors.As(err, &e) && e.Response.StatusCode() == 404 {
188 | 			return false, nil
189 | 		}
190 | 
191 | 		return false, err
192 | 	}
193 | 
194 | 	return true, nil
195 | }
196 | 
197 | func testAccCheckSiteDestroy(s *terraform.State) error {
198 | 	for _, rs := range s.RootModule().Resources {
199 | 		if rs.Type != "ohdear_site" {
200 | 			continue
201 | 		}
202 | 
203 | 		// give the API time to update
204 | 		time.Sleep(5 * time.Second)
205 | 
206 | 		exists, err := doesSiteExists(rs.Primary.ID)
207 | 		if err != nil {
208 | 			return err
209 | 		}
210 | 
211 | 		if exists {
212 | 			return fmt.Errorf("site still exists in Oh Dear: %s", rs.Primary.ID)
213 | 		}
214 | 	}
215 | 	return nil
216 | }
217 | 
218 | func testAccEnsureSiteExists(name string) resource.TestCheckFunc {
219 | 	return func(s *terraform.State) error {
220 | 		rs, ok := s.RootModule().Resources[name]
221 | 		if !ok {
222 | 			return fmt.Errorf("resource not found: %s", name)
223 | 		}
224 | 
225 | 		exists, err := doesSiteExists(rs.Primary.ID)
226 | 		if err != nil {
227 | 			return err
228 | 		}
229 | 
230 | 		if !exists {
231 | 			return fmt.Errorf("resource not found: %s", name)
232 | 		}
233 | 
234 | 		return nil
235 | 	}
236 | }
237 | 
238 | func testAccEnsureChecksEnabled(name string, checksWanted []string) resource.TestCheckFunc {
239 | 	return func(s *terraform.State) error {
240 | 		client := testAccProvider.Meta().(*Config).client
241 | 
242 | 		missingErr := fmt.Errorf("resource not found: %s", name)
243 | 		rs, ok := s.RootModule().Resources[name]
244 | 		if !ok {
245 | 			return missingErr
246 | 		}
247 | 		siteID, _ := strconv.Atoi(rs.Primary.ID)
248 | 		site, _ := client.GetSite(siteID)
249 | 
250 | 		for _, check := range checksWanted {
251 | 			enabled := isCheckEnabled(site, check)
252 | 			if !enabled {
253 | 				return fmt.Errorf("Check %s not enabled for site %s", check, name)
254 | 			}
255 | 		}
256 | 
257 | 		return nil
258 | 	}
259 | }
260 | 
261 | // TODO: merge with enabled (take map of boolean to check all at once)
262 | func testAccEnsureChecksDisabled(name string, checksWanted []string) resource.TestCheckFunc {
263 | 	return func(s *terraform.State) error {
264 | 		client := testAccProvider.Meta().(*Config).client
265 | 
266 | 		rs, ok := s.RootModule().Resources[name]
267 | 		if !ok {
268 | 			return fmt.Errorf("resource not found: %s", name)
269 | 		}
270 | 
271 | 		siteID, _ := strconv.Atoi(rs.Primary.ID)
272 | 		site, err := client.GetSite(siteID)
273 | 		if err != nil {
274 | 			return err
275 | 		}
276 | 
277 | 		for _, check := range checksWanted {
278 | 			if isCheckEnabled(site, check) {
279 | 				return fmt.Errorf("check %s not enabled for site %s", check, name)
280 | 			}
281 | 		}
282 | 
283 | 		return nil
284 | 	}
285 | }
286 | 
287 | // isCheckEnabled checks the site retrieved from OhDear to see whether the
288 | // specified check is present and enabled
289 | func isCheckEnabled(site *ohdear.Site, checkName string) bool {
290 | 	for _, aCheck := range site.Checks {
291 | 		if aCheck.Type == checkName && aCheck.Enabled == true {
292 | 			return true
293 | 		}
294 | 	}
295 | 
296 | 	return false
297 | }
298 | 
299 | // Configs
300 | 
301 | func testAccOhdearSiteConfigBasic(name, url string) string {
302 | 	return fmt.Sprintf(`
303 | resource "ohdear_site" "%s" {
304 |   url = "%s"
305 | }
306 | `, name, url)
307 | }
308 | 
309 | func testAccOhdearSiteConfigTeamID(name, url, team string) string {
310 | 	return fmt.Sprintf(`
311 | resource "ohdear_site" "%s" {
312 |   team_id = %s
313 |   url     = "%s"
314 | }
315 | `, name, team, url)
316 | }
317 | 
318 | func testAccOhdearSiteConfigChecks(name, url string, checks map[string]bool) string {
319 | 	block := []string{}
320 | 	for check, enabled := range checks {
321 | 		block = append(block, fmt.Sprintf("%s = %t", check, enabled))
322 | 	}
323 | 
324 | 	return fmt.Sprintf(`
325 | resource "ohdear_site" "%s" {
326 |   url = "%s"
327 | 
328 |   checks {
329 |     %s
330 |   }
331 | }
332 | `, name, url, strings.Join(block, "\n    "))
333 | }
334 | 


--------------------------------------------------------------------------------
/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/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
  4 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
  5 | github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
  6 | github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
  7 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
  8 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
  9 | github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
 10 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
 11 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
 12 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
 13 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 14 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
 15 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
 16 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
 17 | github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
 18 | github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 19 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
 20 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
 21 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
 22 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 23 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 24 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
 25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 26 | github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
 27 | github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 28 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
 29 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
 30 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
 31 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
 32 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
 33 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 37 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 38 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 39 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 40 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 41 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 42 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 43 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
 44 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 45 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 46 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 47 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
 48 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 49 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
 50 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
 51 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 52 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 53 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 54 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 55 | github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
 56 | github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
 57 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
 58 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 59 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
 60 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 61 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 62 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 63 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 64 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 65 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 66 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 67 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 68 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 69 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 70 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 71 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 72 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 73 | github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
 74 | github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
 75 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 76 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 77 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 78 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
 79 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
 80 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 81 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 82 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 83 | github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0=
 84 | github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM=
 85 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
 86 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 87 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 88 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 89 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 90 | github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
 91 | github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
 92 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
 93 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
 94 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 95 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
 96 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 97 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 98 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 99 | github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=
100 | github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=
101 | github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
102 | github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
103 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
104 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
105 | github.com/hashicorp/terraform-exec v0.23.1 h1:diK5NSSDXDKqHEOIQefBMu9ny+FhzwlwV0xgUTB7VTo=
106 | github.com/hashicorp/terraform-exec v0.23.1/go.mod h1:e4ZEg9BJDRaSalGm2z8vvrPONt0XWG0/tXpmzYTf+dM=
107 | github.com/hashicorp/terraform-json v0.27.1 h1:zWhEracxJW6lcjt/JvximOYyc12pS/gaKSy/wzzE7nY=
108 | github.com/hashicorp/terraform-json v0.27.1/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE=
109 | github.com/hashicorp/terraform-plugin-docs v0.23.0 h1:sipnfD4/9EJBg9zekym+s1H6qmLAKJHhGWBwvN9v/hE=
110 | github.com/hashicorp/terraform-plugin-docs v0.23.0/go.mod h1:J4b5AtMRgJlDrwCQz+G4hKABgHY5m56PnsRmdAzBwW8=
111 | github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
112 | github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
113 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
114 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
115 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4=
116 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU=
117 | github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
118 | github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
119 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
120 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
121 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
122 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
123 | github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
124 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
125 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
126 | github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
127 | github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
128 | github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
129 | github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
130 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
131 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
132 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
133 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
134 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
135 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
136 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
137 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
138 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
139 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
140 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
141 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
142 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
143 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
144 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
145 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
146 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
147 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
148 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
149 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
150 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
151 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
152 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
153 | github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
154 | github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
155 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
156 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
157 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
158 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
159 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
160 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
161 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
162 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
163 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
164 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
165 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
166 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
167 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
168 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
169 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
170 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
171 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
172 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
173 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
174 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
175 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
176 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
177 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
178 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
179 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
180 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
181 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
182 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
183 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
184 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
185 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
186 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
187 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
188 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
189 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
190 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
191 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
192 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
193 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
194 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
195 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
196 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
197 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
198 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
199 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
200 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
201 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
202 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
203 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
204 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
205 | github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
206 | github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
207 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
208 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
209 | github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
210 | github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
211 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
212 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
213 | go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
214 | go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU=
215 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
216 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
217 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
218 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
219 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
220 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
221 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
222 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
223 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
224 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
225 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
226 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
227 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
228 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
229 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
230 | golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
231 | golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
232 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
233 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
234 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
235 | golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
236 | golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
237 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
238 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
239 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
240 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
241 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
242 | golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
243 | golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
244 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
245 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
246 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
247 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
248 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
249 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
250 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
251 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
252 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
253 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
254 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
255 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
257 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
258 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
259 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
260 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
261 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
262 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
263 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
264 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
265 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
266 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
267 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
268 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
269 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
270 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
271 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
272 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
273 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
274 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
275 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
276 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
277 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
278 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
279 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
280 | golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
281 | golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
282 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
283 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
284 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
285 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
286 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
287 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
288 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
289 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
290 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
291 | google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
292 | google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
293 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
294 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
295 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
296 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
297 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
298 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
299 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
300 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
301 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
302 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
303 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
304 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
305 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
306 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
307 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
308 | 


--------------------------------------------------------------------------------