├── examples ├── provider │ └── provider.tf ├── resources │ ├── statuscake_ssl_check │ │ ├── import.sh │ │ └── resource.tf │ ├── statuscake_uptime_check │ │ ├── import.sh │ │ └── resource.tf │ ├── statuscake_contact_group │ │ ├── import.sh │ │ └── resource.tf │ ├── statuscake_heartbeat_check │ │ ├── import.sh │ │ └── resource.tf │ ├── statuscake_maintenance_window │ │ ├── import.sh │ │ └── resource.tf │ └── statuscake_pagespeed_check │ │ ├── import.sh │ │ └── resource.tf └── data-sources │ ├── statuscake_contact_group │ └── data-source.tf │ ├── statuscake_uptime_monitoring_locations │ └── data-source.tf │ └── statuscake_pagespeed_monitoring_locations │ └── data-source.tf ├── terraform-registry-manifest.json ├── tools.go ├── .markdownlint.jsonc ├── .commitlintrc ├── .yamllint ├── .github ├── dependabot.yaml ├── CODEOWNERS ├── workflows │ ├── test.yaml │ ├── publish.yaml │ ├── auto-merge.yaml │ └── policy.yaml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── terraform_logo.svg ├── .editorconfig ├── internal └── provider │ ├── provider_test.go │ ├── diag │ └── helpers.go │ ├── validation │ ├── validations.go │ └── validations_test.go │ ├── utils.go │ ├── data_source_contact_group.go │ ├── provider.go │ ├── data_source_monitoring_locations.go │ ├── resource_contact_group.go │ ├── resource_maintenance_window.go │ ├── resource_heartbeat_check.go │ ├── resource_pagespeed_check.go │ └── resource_ssl_check.go ├── .gitignore ├── .yamlfmt ├── docs ├── data-sources │ ├── contact_group.md │ ├── uptime_monitoring_locations.md │ └── pagespeed_monitoring_locations.md ├── index.md └── resources │ ├── contact_group.md │ ├── maintenance_window.md │ ├── heartbeat_check.md │ ├── pagespeed_check.md │ ├── ssl_check.md │ └── uptime_check.md ├── Makefile ├── main.go ├── README.md ├── goreleaser.yaml ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── go.mod ├── LICENSE └── go.sum /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "statuscake" { 2 | api_token = "my-api-token" 3 | } 4 | -------------------------------------------------------------------------------- /examples/resources/statuscake_ssl_check/import.sh: -------------------------------------------------------------------------------- 1 | terraform import statuscake_ssl_check.example_com 1234 2 | -------------------------------------------------------------------------------- /examples/resources/statuscake_uptime_check/import.sh: -------------------------------------------------------------------------------- 1 | terraform import statuscake_uptime_check.example_com 1234 2 | -------------------------------------------------------------------------------- /examples/resources/statuscake_contact_group/import.sh: -------------------------------------------------------------------------------- 1 | terraform import statuscake_contact_group.operations_team 1234 2 | -------------------------------------------------------------------------------- /examples/resources/statuscake_heartbeat_check/import.sh: -------------------------------------------------------------------------------- 1 | terraform import statuscake_heartbeat_check.example_com 1234 2 | -------------------------------------------------------------------------------- /examples/resources/statuscake_maintenance_window/import.sh: -------------------------------------------------------------------------------- 1 | terraform import statuscake_maintenance_window.weekends 1234 2 | -------------------------------------------------------------------------------- /examples/resources/statuscake_pagespeed_check/import.sh: -------------------------------------------------------------------------------- 1 | terraform import statuscake_pagespeed_check.example_com 1234 2 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["5.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-inline-html": { 4 | "allowed_elements": [ 5 | "a", 6 | "img" 7 | ] 8 | }, 9 | "line-length": { 10 | "tables": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional", 4 | ], 5 | "rules": { 6 | "body-max-line-length": [2, "always", 72], 7 | "header-max-length": [2, "always", 50], 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /examples/data-sources/statuscake_contact_group/data-source.tf: -------------------------------------------------------------------------------- 1 | data "statuscake_contact_group" "developers" { 2 | id = "123456" 3 | } 4 | 5 | output "developers_contact_group_name" { 6 | value = data.statuscake_contact_group.developers.name 7 | } 8 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | # vi: ft=yaml 2 | --- 3 | extends: default 4 | rules: 5 | document-start: 6 | level: error 7 | indentation: 8 | indent-sequences: false 9 | spaces: 2 10 | line-length: false 11 | quoted-strings: 12 | quote-type: double 13 | required: only-when-needed 14 | truthy: disable 15 | -------------------------------------------------------------------------------- /examples/data-sources/statuscake_uptime_monitoring_locations/data-source.tf: -------------------------------------------------------------------------------- 1 | data "statuscake_uptime_monitoring_locations" "uptime" { 2 | region_code = "GBR" 3 | } 4 | 5 | output "uptime_monitoring_location_ips" { 6 | value = toset([for loc in data.statuscake_uptime_monitoring_locations.uptime.locations : loc.ipv4]) 7 | } 8 | -------------------------------------------------------------------------------- /examples/data-sources/statuscake_pagespeed_monitoring_locations/data-source.tf: -------------------------------------------------------------------------------- 1 | data "statuscake_pagespeed_monitoring_locations" "pagespeed" { 2 | region_code = "GB" 3 | } 4 | 5 | output "pagespeed_monitoring_location_ips" { 6 | value = toset([for loc in data.statuscake_pagespeed_monitoring_locations.pagespeed.locations : loc.ipv4]) 7 | } 8 | -------------------------------------------------------------------------------- /examples/resources/statuscake_contact_group/resource.tf: -------------------------------------------------------------------------------- 1 | resource "statuscake_contact_group" "operations_team" { 2 | name = "Operations Team" 3 | ping_url = "https://www.example.com" 4 | 5 | email_addresses = [ 6 | "johnsmith@example.com", 7 | "janesmith@example.com", 8 | ] 9 | } 10 | 11 | output "operations_team_contact_group_id" { 12 | value = statuscake_contact_group.operations_team.id 13 | } 14 | -------------------------------------------------------------------------------- /examples/resources/statuscake_maintenance_window/resource.tf: -------------------------------------------------------------------------------- 1 | resource "statuscake_maintenance_window" "weekends" { 2 | end = "2022-01-30T23:59:59Z" 3 | name = "Weekends" 4 | repeat_interval = "1w" 5 | start = "2022-01-29T00:00:00Z" 6 | timezone = "UTC" 7 | 8 | tags = [ 9 | "production" 10 | ] 11 | 12 | tests = [ 13 | statuscake_uptime_check.statuscake_com.id, 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | commit-message: 9 | prefix: ci(deps) 10 | - package-ecosystem: gomod 11 | directory: / 12 | schedule: 13 | interval: monthly 14 | commit-message: 15 | prefix: chore(deps) 16 | ignore: 17 | - dependency-name: "*" 18 | update-types: 19 | - version-update:semver-major 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.{go,mod}] 16 | indent_style = tab 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | 21 | [{GNU,}Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /examples/resources/statuscake_heartbeat_check/resource.tf: -------------------------------------------------------------------------------- 1 | resource "statuscake_heartbeat_check" "example" { 2 | name = "Example" 3 | period = 1800 4 | 5 | contact_groups = [ 6 | statuscake_contact_group.operations_team.id 7 | ] 8 | 9 | tags = [ 10 | "production", 11 | ] 12 | } 13 | 14 | output "example_heartbeat_check_id" { 15 | value = statuscake_heartbeat_check.example.id 16 | } 17 | 18 | output "example_heartbeat_check_url" { 19 | value = statuscake_heartbeat_check.example.check_url 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. Unless a 2 | # later match takes precedence, these owner(s) will be requested for review 3 | # when someone opens a pull request. 4 | * @StatusCakeDev/developers 5 | 6 | # Order is important; the last matching pattern takes the most precedence. When 7 | # someone opens a pull request that only modifies the following paths, only 8 | # these owner(s) and not the global owner(s) will be requested for a review. 9 | .github/ @StatusCakeDev/operations 10 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | 8 | "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider" 9 | ) 10 | 11 | var testProviders = map[string]*schema.Provider{ 12 | "statuscake": provider.Provider(), 13 | } 14 | 15 | func TestProvider(t *testing.T) { 16 | if err := testProviders["statuscake"].InternalValidate(); err != nil { 17 | t.Errorf("failed to validate provider: %+v", err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/resources/statuscake_pagespeed_check/resource.tf: -------------------------------------------------------------------------------- 1 | resource "statuscake_pagespeed_check" "example_com" { 2 | check_interval = 300 3 | name = "Example" 4 | region = "UK" 5 | 6 | alert_config { 7 | alert_bigger = "5000" 8 | } 9 | 10 | contact_groups = [ 11 | statuscake_contact_group.operations_team.id 12 | ] 13 | 14 | monitored_resource { 15 | address = "https://www.example.com" 16 | } 17 | } 18 | 19 | output "example_com_pagespeed_check_id" { 20 | value = statuscake_pagespeed_check.example_com.id 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Terraform artefacts 15 | .terraform.lock.hcl 16 | .terraform.tfstate.lock.info 17 | terraform.tfplan 18 | terraform.tfstate 19 | ./*.tfstate 20 | .terraform/ 21 | 22 | # Executables 23 | terraform-provider-statuscake* 24 | 25 | # Test exclusions 26 | !command/test-fixtures/**/*.tfstate 27 | !command/test-fixtures/**/.terraform/ 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - gh-readonly-queue/** 8 | - master 9 | jobs: 10 | test: 11 | name: Run tests 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions/setup-go@v6 22 | with: 23 | go-version: "1.24" 24 | - run: go test -v -cover ./... 25 | timeout-minutes: 10 26 | env: 27 | TF_ACC: "1" 28 | -------------------------------------------------------------------------------- /examples/resources/statuscake_ssl_check/resource.tf: -------------------------------------------------------------------------------- 1 | resource "statuscake_ssl_check" "example_com" { 2 | check_interval = 600 3 | user_agent = "terraform managed SSL check" 4 | 5 | alert_config { 6 | alert_at = [7, 14, 21] 7 | on_broken = false 8 | on_expiry = true 9 | on_mixed = false 10 | on_reminder = true 11 | } 12 | 13 | contact_groups = [ 14 | statuscake_contact_group.operations_team.id 15 | ] 16 | 17 | monitored_resource { 18 | address = "https://www.example.com" 19 | } 20 | } 21 | 22 | output "example_com_ssl_check_id" { 23 | value = statuscake_ssl_check.example_com.id 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always 11 | frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've 18 | considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.yamlfmt: -------------------------------------------------------------------------------- 1 | # vi: ft=yaml 2 | --- 3 | formatter: 4 | type: basic 5 | # There is a "bug" with the behaviour of this option that will move file head 6 | # comments below the initial document start. For example front matter is 7 | # rearraged and therefore misplaced in the file. This behaviour prevents 8 | # `ymalfmt` from being run in CI or automatically in code editors. 9 | include_document_start: true 10 | indentless_arrays: true 11 | pad_line_comments: 2 12 | # There is missing functionality in `yamlfmt` to remove redundantly quoted 13 | # strings from YAML documents, and prefer the use of double quotes (or 14 | # whatever is configured). This means further manual effort is required to 15 | # correct files. 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: publish 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | jobs: 8 | goreleaser: 9 | name: Publish artefacts to GitHub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-go@v6 16 | with: 17 | go-version: "1.21" 18 | - id: import_gpg 19 | uses: crazy-max/ghaction-import-gpg@v6 20 | with: 21 | gpg_private_key: ${{ secrets.ARTEFACT_SIGNING_KEY }} 22 | passphrase: ${{ secrets.ARTEFACT_SIGNING_KEY_PASSPHRASE }} 23 | - uses: goreleaser/goreleaser-action@v6 24 | with: 25 | version: latest 26 | args: release --clean 27 | env: 28 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behaviour: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /docs/data-sources/contact_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_contact_group Data Source - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_contact_group (Data Source) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "statuscake_contact_group" "developers" { 17 | id = "123456" 18 | } 19 | 20 | output "developers_contact_group_name" { 21 | value = data.statuscake_contact_group.developers.name 22 | } 23 | ``` 24 | 25 | 26 | ## Schema 27 | 28 | ### Required 29 | 30 | - `id` (String) Contact group ID 31 | 32 | ### Read-Only 33 | 34 | - `email_addresses` (Set of String) List of email addresses 35 | - `integrations` (Set of String) List of integration IDs 36 | - `mobile_numbers` (Set of String) List of international format mobile phone numbers 37 | - `name` (String) Name of the contact group 38 | - `ping_url` (String) URL or IP address of an endpoint to push uptime events. Currently this only supports HTTP GET endpoints 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | .ONESHELL: 3 | .SHELLFLAGS := -eu -o pipefail -c 4 | .DEFAULT_GOAL := all 5 | .DELETE_ON_ERROR: 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | GO ?= go 9 | TESTARGS ?= 10 | BINARY_NAME := terraform-provider-statuscake 11 | 12 | ifeq ($(origin .RECIPEPREFIX), undefined) 13 | $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later) 14 | endif 15 | .RECIPEPREFIX = 16 | 17 | .PHONY: all 18 | all: build testacc 19 | 20 | .PHONY: help 21 | help: 22 | @echo "Usage: make " 23 | @echo 24 | @echo "Targets:" 25 | @echo " all Build and test the project (default)" 26 | @echo " build Build the project" 27 | @echo " testacc Run acceptance tests" 28 | @echo " clean Clean the project" 29 | 30 | .PHONY: build 31 | build: $(BINARY_NAME) 32 | @echo "done" 33 | 34 | $(BINARY_NAME): 35 | @echo "building provider" 36 | @go build -o $@ 37 | 38 | .PHONY: docs 39 | docs: 40 | @go generate ./... 41 | 42 | .PHONY: testacc 43 | testacc: 44 | TF_ACC=1 $(GO) test ./... -v $(TESTARGS) -timeout 120m 45 | 46 | .PHONY: clean 47 | clean: 48 | @rm -f $(BINARY_NAME) 49 | -------------------------------------------------------------------------------- /docs/data-sources/uptime_monitoring_locations.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_uptime_monitoring_locations Data Source - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_uptime_monitoring_locations (Data Source) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "statuscake_uptime_monitoring_locations" "uptime" { 17 | region_code = "GBR" 18 | } 19 | 20 | output "uptime_monitoring_location_ips" { 21 | value = toset([for loc in data.statuscake_uptime_monitoring_locations.uptime.locations : loc.ipv4]) 22 | } 23 | ``` 24 | 25 | 26 | ## Schema 27 | 28 | ### Optional 29 | 30 | - `region_code` (String) Location region code 31 | 32 | ### Read-Only 33 | 34 | - `id` (String) The ID of this resource. 35 | - `locations` (List of Object) List of monitoring locations (see [below for nested schema](#nestedatt--locations)) 36 | 37 | 38 | ### Nested Schema for `locations` 39 | 40 | Read-Only: 41 | 42 | - `description` (String) 43 | - `ipv4` (String) 44 | - `ipv6` (String) 45 | - `region` (String) 46 | - `region_code` (String) 47 | - `status` (String) 48 | -------------------------------------------------------------------------------- /docs/data-sources/pagespeed_monitoring_locations.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_pagespeed_monitoring_locations Data Source - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_pagespeed_monitoring_locations (Data Source) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "statuscake_pagespeed_monitoring_locations" "pagespeed" { 17 | region_code = "GB" 18 | } 19 | 20 | output "pagespeed_monitoring_location_ips" { 21 | value = toset([for loc in data.statuscake_pagespeed_monitoring_locations.pagespeed.locations : loc.ipv4]) 22 | } 23 | ``` 24 | 25 | 26 | ## Schema 27 | 28 | ### Optional 29 | 30 | - `region_code` (String) Location region code 31 | 32 | ### Read-Only 33 | 34 | - `id` (String) The ID of this resource. 35 | - `locations` (List of Object) List of monitoring locations (see [below for nested schema](#nestedatt--locations)) 36 | 37 | 38 | ### Nested Schema for `locations` 39 | 40 | Read-Only: 41 | 42 | - `description` (String) 43 | - `ipv4` (String) 44 | - `ipv6` (String) 45 | - `region` (String) 46 | - `region_code` (String) 47 | - `status` (String) 48 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 9 | 10 | "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider" 11 | ) 12 | 13 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 14 | 15 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 16 | // ensure the documentation is formatted properly. 17 | // 18 | //go:generate terraform fmt -recursive ./examples/ 19 | 20 | // Run the docs generation tool, check its repository for more information on how it works and how docs 21 | // can be customized. 22 | // 23 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 24 | 25 | func main() { 26 | var debug bool 27 | 28 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 29 | flag.Parse() 30 | 31 | opts := &plugin.ServeOpts{ProviderFunc: provider.Provider} 32 | 33 | if debug { 34 | if err := plugin.Debug(context.Background(), "registry.terraform.io/StatusCakeDev/statuscake", opts); err != nil { 35 | log.Fatal(err.Error()) 36 | } 37 | 38 | return 39 | } 40 | 41 | plugin.Serve(opts) 42 | } 43 | -------------------------------------------------------------------------------- /examples/resources/statuscake_uptime_check/resource.tf: -------------------------------------------------------------------------------- 1 | resource "statuscake_uptime_check" "example_com" { 2 | check_interval = 30 3 | confirmation = 3 4 | name = "Example" 5 | trigger_rate = 10 6 | 7 | contact_groups = [ 8 | statuscake_contact_group.operations_team.id 9 | ] 10 | 11 | http_check { 12 | enable_cookies = true 13 | follow_redirects = true 14 | timeout = 20 15 | user_agent = "terraform managed uptime check" 16 | validate_ssl = true 17 | 18 | basic_authentication { 19 | password = "password" 20 | username = "username" 21 | } 22 | 23 | content_matchers { 24 | content = "Welcome" 25 | include_headers = true 26 | matcher = "CONTAINS_STRING" 27 | } 28 | 29 | request_headers = { 30 | Authorization = "bearer 123456" 31 | } 32 | 33 | status_codes = [ 34 | "202", 35 | "404", 36 | "405", 37 | ] 38 | } 39 | 40 | monitored_resource { 41 | address = "https://www.example.com" 42 | } 43 | 44 | regions = [ 45 | "london", 46 | "london", 47 | "paris", 48 | ] 49 | 50 | tags = [ 51 | "production", 52 | ] 53 | } 54 | 55 | output "example_com_uptime_check_id" { 56 | value = statuscake_uptime_check.example_com.id 57 | } 58 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake Provider" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake Provider 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | provider "statuscake" { 17 | api_token = "my-api-token" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `api_token` (String) The API token for operations. This can also be provided as an environment variable `STATUSCAKE_API_TOKEN` 27 | 28 | ### Optional 29 | 30 | - `max_backoff` (Number) Maximum backoff period in seconds after failed API calls. This can also be provided as an environment variable `STATUSCAKE_MAX_BACKOFF` 31 | - `min_backoff` (Number) Minimum backoff period in seconds after failed API calls. This can also be provided as an environment variable `STATUSCAKE_MIN_BACKOFF` 32 | - `retries` (Number) Maximum number of retries to perform when an API request fails. This can also be provided as an environment variable `STATUSCAKE_RETRIES` 33 | - `rps` (Number) RPS limit to apply when making calls to the API. This can also be provided as an environment variable `STATUSCAKE_RPS` 34 | - `statuscake_custom_endpoint` (String) Custom endpoint to which request will be made. This can also be provided as an environment variable `STATUCAKE_CUSTOM_ENDPOINT` 35 | -------------------------------------------------------------------------------- /internal/provider/diag/helpers.go: -------------------------------------------------------------------------------- 1 | package diag 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/StatusCakeDev/statuscake-go" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | ) 9 | 10 | // FromErr will convert an error into a Diagnostics. Each Diagnostic entry will 11 | // have the summary line prefixed with a contextual message. 12 | func FromErr(message string, err error) diag.Diagnostics { 13 | if err == nil { 14 | return nil 15 | } 16 | return diagnostics(message, err) 17 | } 18 | 19 | func diagnostics(message string, err error) diag.Diagnostics { 20 | errs := statuscake.Errors(err) 21 | if len(errs) == 0 { 22 | return fromErr(message, err) 23 | } 24 | return violations(message, err, errs) 25 | } 26 | 27 | func fromErr(message string, err error) diag.Diagnostics { 28 | return diag.Diagnostics{ 29 | diag.Diagnostic{ 30 | Severity: diag.Error, 31 | Summary: message + ": " + err.Error(), 32 | }, 33 | } 34 | } 35 | 36 | func violations(message string, err error, errs map[string][]string) diag.Diagnostics { 37 | var diags diag.Diagnostics 38 | for field, violations := range errs { 39 | diags = append(diags, diag.Diagnostic{ 40 | Severity: diag.Error, 41 | Summary: message + ": " + err.Error() + ": " + field + " contains violations", 42 | 43 | // TODO: Use AttributePath to indicate validation errors. 44 | Detail: strings.Join(violations, "; "), 45 | }) 46 | } 47 | return diags 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StatusCake Terraform Provider ![test](https://github.com/StatusCakeDev/terraform-provider-statuscake/workflows/test/badge.svg) 2 | 3 | 4 | Terraform logo 6 | 7 | 8 | [![Support][support-badge]][support] 9 | 10 | [support-badge]: https://img.shields.io/badge/support-terraform--statuscake-623CE4.svg?style=flat 11 | [support]: https://developers.statuscake.com/guides/terraform/introduction 12 | 13 | - Website: [terraform.io](https://terraform.io) 14 | - Tutorials: [learn.hashicorp.com](https://learn.hashicorp.com/terraform?track=getting-started#getting-started) 15 | - Support: [developers.statuscake.com](https://developers.statuscake.com/guides/terraform/introduction) 16 | - Chat: [gitter](https://gitter.im/hashicorp-terraform/Lobby) 17 | - Mailing List: [Google Groups](http://groups.google.com/group/terraform-tool) 18 | 19 | ## Prerequisites 20 | 21 | You will need the following things properly installed on your computer: 22 | 23 | - [Terraform](https://www.terraform.io/downloads.html) (1.0+) 24 | 25 | ## Installation 26 | 27 | ### Documentation 28 | 29 | Full, comprehensive documentation is available on the [Terraform 30 | website](https://registry.terraform.io/providers/StatusCakeDev/statuscake/latest/docs) 31 | 32 | ## License 33 | 34 | This project is licensed under the [Mozilla Public License](LICENSE). 35 | -------------------------------------------------------------------------------- /docs/resources/contact_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_contact_group Resource - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_contact_group (Resource) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "statuscake_contact_group" "operations_team" { 17 | name = "Operations Team" 18 | ping_url = "https://www.example.com" 19 | 20 | email_addresses = [ 21 | "johnsmith@example.com", 22 | "janesmith@example.com", 23 | ] 24 | } 25 | 26 | output "operations_team_contact_group_id" { 27 | value = statuscake_contact_group.operations_team.id 28 | } 29 | ``` 30 | 31 | 32 | ## Schema 33 | 34 | ### Required 35 | 36 | - `name` (String) Name of the contact group 37 | 38 | ### Optional 39 | 40 | - `email_addresses` (Set of String) List of email addresses 41 | - `integrations` (Set of String) List of integration IDs 42 | - `mobile_numbers` (Set of String) List of international format mobile phone numbers 43 | - `ping_url` (String) URL or IP address of an endpoint to push uptime events. Currently this only supports HTTP GET endpoints 44 | 45 | ### Read-Only 46 | 47 | - `id` (String) The ID of this resource. 48 | 49 | ## Import 50 | 51 | Import is supported using the following syntax: 52 | 53 | ```shell 54 | terraform import statuscake_contact_group.operations_team 1234 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/resources/maintenance_window.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_maintenance_window Resource - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_maintenance_window (Resource) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "statuscake_maintenance_window" "weekends" { 17 | end = "2022-01-30T23:59:59Z" 18 | name = "Weekends" 19 | repeat_interval = "1w" 20 | start = "2022-01-29T00:00:00Z" 21 | timezone = "UTC" 22 | 23 | tags = [ 24 | "production" 25 | ] 26 | 27 | tests = [ 28 | statuscake_uptime_check.statuscake_com.id, 29 | ] 30 | } 31 | ``` 32 | 33 | 34 | ## Schema 35 | 36 | ### Required 37 | 38 | - `end` (String) End of the maintenance window (RFC3339 format) 39 | - `name` (String) Name of the maintenance window 40 | - `start` (String) Start of the maintenance window (RFC3339 format) 41 | - `timezone` (String) Standard timezone associated with this maintenance window 42 | 43 | ### Optional 44 | 45 | - `repeat_interval` (String) How often the maintenance window should occur 46 | - `tags` (Set of String) List of tags used to include matching uptime checks in this maintenance window 47 | - `tests` (Set of String) List of uptime check IDs explicitly included in this maintenance window 48 | 49 | ### Read-Only 50 | 51 | - `id` (String) The ID of this resource. 52 | 53 | ## Import 54 | 55 | Import is supported using the following syntax: 56 | 57 | ```shell 58 | terraform import statuscake_maintenance_window.weekends 1234 59 | ``` 60 | -------------------------------------------------------------------------------- /goreleaser.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Visit https://goreleaser.com for documentation on how to customize this 3 | # behavior. 4 | archives: 5 | - format: zip 6 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 7 | before: 8 | hooks: 9 | - go mod download 10 | builds: 11 | - binary: "{{ .ProjectName }}_v{{ .Version }}" 12 | env: 13 | # goreleaser does not work with CGO, it could also complicate usage by users 14 | # in CI/CD systems like Terraform Cloud where they are unable to install 15 | # libraries. 16 | - CGO_ENABLED=0 17 | flags: 18 | - -trimpath 19 | ldflags: 20 | - -s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }} 21 | goarch: 22 | - "386" 23 | - amd64 24 | - arm 25 | - arm64 26 | goos: 27 | - darwin 28 | - freebsd 29 | - linux 30 | - windows 31 | ignore: 32 | - goarch: "386" 33 | goos: darwin 34 | mod_timestamp: "{{ .CommitTimestamp }}" 35 | changelog: 36 | skip: true 37 | checksum: 38 | algorithm: sha256 39 | extra_files: 40 | - glob: terraform-registry-manifest.json 41 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 42 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 43 | release: 44 | extra_files: 45 | - glob: terraform-registry-manifest.json 46 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 47 | signs: 48 | - artifacts: checksum 49 | args: 50 | # If you are using this in a GitHub action or some other automated pipeline, 51 | # you need to pass the batch flag to indicate its not interactive. 52 | - --batch 53 | - --local-user 54 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 55 | - --output 56 | - ${signature} 57 | - --detach-sign 58 | - ${artifact} 59 | -------------------------------------------------------------------------------- /docs/resources/heartbeat_check.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_heartbeat_check Resource - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_heartbeat_check (Resource) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "statuscake_heartbeat_check" "example" { 17 | name = "Example" 18 | period = 1800 19 | 20 | contact_groups = [ 21 | statuscake_contact_group.operations_team.id 22 | ] 23 | 24 | tags = [ 25 | "production", 26 | ] 27 | } 28 | 29 | output "example_heartbeat_check_id" { 30 | value = statuscake_heartbeat_check.example.id 31 | } 32 | 33 | output "example_heartbeat_check_url" { 34 | value = statuscake_heartbeat_check.example.check_url 35 | } 36 | ``` 37 | 38 | 39 | ## Schema 40 | 41 | ### Required 42 | 43 | - `name` (String) Name of the check 44 | - `period` (Number) Number of seconds since the last ping before the check is considered down. 45 | 46 | ### Optional 47 | 48 | - `contact_groups` (Set of String) List of contact group IDs 49 | - `monitored_resource` (Block List, Max: 1) Monitored resource configuration block. This describes the server under test (see [below for nested schema](#nestedblock--monitored_resource)) 50 | - `paused` (Boolean) Whether the check should be run 51 | - `tags` (Set of String) List of tags 52 | 53 | ### Read-Only 54 | 55 | - `check_url` (String) URL of the heartbeat check 56 | - `id` (String) The ID of this resource. 57 | 58 | 59 | ### Nested Schema for `monitored_resource` 60 | 61 | Optional: 62 | 63 | - `host` (String) Name of the hosting provider 64 | 65 | ## Import 66 | 67 | Import is supported using the following syntax: 68 | 69 | ```shell 70 | terraform import statuscake_heartbeat_check.example_com 1234 71 | ``` 72 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: auto-merge 3 | on: 4 | pull_request: 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | jobs: 9 | check-can-auto-merge: 10 | if: github.triggering_actor == 'dependabot[bot]' 11 | name: Check if PR can be auto-merged 12 | runs-on: ubuntu-latest 13 | outputs: 14 | can-auto-merge: ${{ steps.can-auto-merge.outputs.approve }} 15 | steps: 16 | - uses: dependabot/fetch-metadata@v2.4.0 17 | id: dependabot-metadata 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | - if: | 21 | steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || 22 | steps.dependabot-metadata.outputs.package-ecosystem == 'github_actions' 23 | id: can-auto-merge 24 | run: echo "approve=true" >> $GITHUB_OUTPUT 25 | auto-merge: 26 | needs: check-can-auto-merge 27 | if: needs.check-can-auto-merge.outputs.can-auto-merge == 'true' 28 | name: Auto merge pull requests 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: dependabot/fetch-metadata@v2.4.0 32 | id: dependabot-metadata 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | - name: Approve Dependabot PR 36 | run: gh pr review --approve "${PR_URL}" 37 | env: 38 | PR_URL: ${{ github.event.pull_request.html_url }} 39 | # Because we enforce PRs be approved by a code owner, a token from the 40 | # operations team must be used to approve the PR. This token is from the 41 | # StatusCake machine account which has been given membership to the 42 | # operations team. 43 | GITHUB_TOKEN: ${{ secrets.MACHINE_TOKEN }} 44 | - name: Enable auto-merge for Dependabot PR 45 | run: gh pr merge --auto --merge "${PR_URL}" 46 | env: 47 | PR_URL: ${{ github.event.pull_request.html_url }} 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | Contributions are welcome as we strive to make this application as useful as 4 | possible for everyone. However time is not always on our side, and changes may 5 | not be reviewed or merged in a timely manner. 6 | 7 | If this application is found to be missing in functionality, please open an 8 | issue describing the proposed change - discussing changes ahead of time reduces 9 | friction within pull requests. 10 | 11 | ## Prerequisites 12 | 13 | You will need the following things properly installed on your computer: 14 | 15 | - [Git](https://git-scm.com/) 16 | - [Go](https://go.dev/) (1.21+) 17 | - [Terraform](https://www.terraform.io/) 18 | - [Make](https://en.wikipedia.org/wiki/Make_(software)) (optional) 19 | 20 | ## Installation 21 | 22 | If you wish to work on the provider, you'll first need 23 | [Go](http://www.golang.org) installed on your machine (version 1.19+ is 24 | *required*). You'll also need to correctly setup a 25 | [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding 26 | `$GOPATH/bin` to your `$PATH`. 27 | 28 | To compile the provider, run `make build`. This will build the provider that 29 | should then be moved to the `$GOPATH/bin` directory. 30 | 31 | ```sh 32 | make build 33 | mv terraform-provider-statuscake $GOPATH/bin/terraform-provider-statuscake 34 | ``` 35 | 36 | To use the compiled binary the following must be included in the 37 | `~/.terraformrc` file, having replaced `FULL_PATH_TO_GO_BIN` with the full 38 | directory path to the `$GOPATH/bin` directory. This informs the Terraform CLI 39 | tool to lookup the binary in the `$GOPATH` instead of the regular location. 40 | 41 | ```terraformrc 42 | provider_installation { 43 | dev_overrides { 44 | "statuscakedev/statuscake" = "FULL_PATH_TO_GO_BIN" 45 | } 46 | direct {} 47 | } 48 | ``` 49 | 50 | ## Running tests 51 | 52 | - `make testacc` 53 | 54 | ## Making Changes 55 | 56 | For additional contributing guidelines visit 57 | [devhandbook.org](https://devhandbook.org/contributing) 58 | -------------------------------------------------------------------------------- /internal/provider/validation/validations.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "net/mail" 6 | "strconv" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | ) 10 | 11 | // IsEmailAddress is a SchemaValidateFunc that tests if the provided value is 12 | // of type string and matches the format of a valid email address. 13 | func IsEmailAddress(i interface{}, k string) ([]string, []error) { 14 | v, ok := i.(string) 15 | if !ok { 16 | return nil, []error{fmt.Errorf("expected type of %q to be string", k)} 17 | } 18 | 19 | if v == "" { 20 | return nil, []error{fmt.Errorf("expected %q email address to not be empty, got %q", k, i)} 21 | } 22 | 23 | if _, err := mail.ParseAddress(v); err != nil { 24 | return nil, []error{fmt.Errorf("expected %q to be a valid email address, got %q: %+v", k, v, err)} 25 | } 26 | 27 | return nil, nil 28 | } 29 | 30 | // StringIsNumerical is a SchemaValidateFunc that tests if the provided value 31 | // is of type string and represents a numerical value. 32 | func StringIsNumerical(i interface{}, k string) ([]string, []error) { 33 | v, ok := i.(string) 34 | if !ok { 35 | return nil, []error{fmt.Errorf("expected type of %q to be string", k)} 36 | } 37 | 38 | if v == "" { 39 | return nil, []error{fmt.Errorf("expected %q number to not be empty, got %q", k, i)} 40 | } 41 | 42 | if _, err := strconv.Atoi(v); err != nil { 43 | return nil, []error{fmt.Errorf("expected %q to be a valid number, got %q: %+v", k, v, err)} 44 | } 45 | 46 | return nil, nil 47 | } 48 | 49 | // Int32InSlice returns a SchemaValidateFunc that tests if the provided value 50 | // is of type int32 and matches the value of an element in the valid slice. 51 | func Int32InSlice(valid []int32) schema.SchemaValidateFunc { 52 | return func(i interface{}, k string) ([]string, []error) { 53 | v, ok := i.(int) 54 | if !ok { 55 | return nil, []error{fmt.Errorf("expected type of %q to be integer", k)} 56 | } 57 | 58 | for _, validInt := range valid { 59 | if int32(v) == validInt { 60 | return nil, nil 61 | } 62 | } 63 | 64 | return nil, []error{fmt.Errorf("expected %q to be one of %+v, got %d", k, valid, v)} 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/provider/utils.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | ) 8 | 9 | func convertStringSet(set *schema.Set) []string { 10 | s := make([]string, set.Len()) 11 | for i, v := range set.List() { 12 | s[i] = v.(string) 13 | } 14 | 15 | return s 16 | } 17 | 18 | func convertInt32Set(set *schema.Set) []int32 { 19 | s := make([]int32, set.Len()) 20 | for i, v := range set.List() { 21 | s[i] = int32(v.(int)) 22 | } 23 | 24 | return s 25 | } 26 | 27 | func convertStringList(list []interface{}) []string { 28 | s := make([]string, len(list)) 29 | for i, v := range list { 30 | s[i] = v.(string) 31 | } 32 | 33 | return s 34 | } 35 | 36 | func stringElem(v interface{}) string { 37 | val := reflect.Indirect(reflect.ValueOf(v)) 38 | if v == nil || isEmptyValue(val) || val.IsZero() { 39 | return "" 40 | } 41 | 42 | return val.Interface().(string) 43 | } 44 | 45 | func isValid(v interface{}) bool { 46 | return !isEmptyValue(reflect.ValueOf(v)) 47 | } 48 | 49 | // https://github.com/hashicorp/terraform-provider-google/commit/9900e6a4c70294db07dec023c9da7e27a12ee464#diff-16751bdc2e307bd9d601bfeb3d2e62100c18679a6f7633cc5afca8d783a1dd4cR61 50 | func isEmptyValue(v reflect.Value) bool { 51 | if !v.IsValid() { 52 | return true 53 | } 54 | 55 | switch v.Kind() { 56 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 57 | return v.Len() == 0 58 | case reflect.Bool: 59 | return !v.Bool() 60 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 61 | return v.Int() == 0 62 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 63 | return v.Uint() == 0 64 | case reflect.Float32, reflect.Float64: 65 | return v.Float() == 0 66 | case reflect.Interface, reflect.Ptr: 67 | return v.IsNil() 68 | } 69 | return false 70 | } 71 | 72 | // merge returns a new map with all the keys specified within each arguement. 73 | // Keys will never be overriden once set. 74 | func merge(maps ...map[string]interface{}) map[string]interface{} { 75 | merged := make(map[string]interface{}) 76 | for _, m := range maps { 77 | for k, v := range m { 78 | if _, ok := merged[k]; ok { 79 | continue 80 | } 81 | merged[k] = v 82 | } 83 | } 84 | return merged 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/policy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: policy 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | # This is run on merge queue branches as there are jobs listed in this 8 | # action that are often required in branch protection rules. Due to guards 9 | # below certain jobs may not run in merge queue branches but will still 10 | # return a status indicating a pass. 11 | - gh-readonly-queue/** 12 | - master 13 | jobs: 14 | check-merge-commits: 15 | if: github.event_name == 'pull_request' 16 | name: Check merge commits 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | with: 21 | fetch-depth: 0 22 | - run: | 23 | git config --global --add safe.directory /github/workspace 24 | merge_commits=$(git rev-list --merges "origin/$GITHUB_BASE_REF".."origin/$GITHUB_HEAD_REF") 25 | if [ -n "$merge_commits" ]; then 26 | echo "Error: merge commits found in $GITHUB_BASE_REF..$GITHUB_HEAD_REF" 27 | for merge_commit in $merge_commits; do 28 | echo "$merge_commit" 29 | done 30 | exit 1 31 | fi 32 | commit-message-style: 33 | if: github.event_name == 'pull_request' && github.triggering_actor != 'dependabot[bot]' 34 | name: Check commit message style 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v6 38 | with: 39 | fetch-depth: 0 40 | - uses: actions/setup-node@v6 41 | with: 42 | node-version: 19.x 43 | - name: Install base config 44 | run: npm install @commitlint/config-conventional 45 | - name: Validate commit messages 46 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to HEAD --verbose 47 | markdown-style: 48 | name: Check markdown style 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v6 52 | with: 53 | fetch-depth: 0 54 | - uses: actions/setup-node@v6 55 | with: 56 | node-version: 19.x 57 | - name: Validate markdown 58 | run: npx markdownlint-cli2 **/*.md "#docs/**/*.md" 59 | yaml-style: 60 | name: Check YAML style 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v6 64 | - uses: actions/setup-python@v6 65 | with: 66 | python-version: "3.10" 67 | - name: Install yamllint 68 | run: pip install --user yamllint 69 | - name: Validate YAML 70 | run: yamllint . 71 | -------------------------------------------------------------------------------- /docs/resources/pagespeed_check.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_pagespeed_check Resource - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_pagespeed_check (Resource) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "statuscake_pagespeed_check" "example_com" { 17 | check_interval = 300 18 | name = "Example" 19 | region = "UK" 20 | 21 | alert_config { 22 | alert_bigger = "5000" 23 | } 24 | 25 | contact_groups = [ 26 | statuscake_contact_group.operations_team.id 27 | ] 28 | 29 | monitored_resource { 30 | address = "https://www.example.com" 31 | } 32 | } 33 | 34 | output "example_com_pagespeed_check_id" { 35 | value = statuscake_pagespeed_check.example_com.id 36 | } 37 | ``` 38 | 39 | 40 | ## Schema 41 | 42 | ### Required 43 | 44 | - `alert_config` (Block List, Min: 1, Max: 1) Alert configuration block. An empty block disables all alerts (see [below for nested schema](#nestedblock--alert_config)) 45 | - `check_interval` (Number) Number of seconds between checks 46 | - `monitored_resource` (Block List, Min: 1, Max: 1) Monitored resource configuration block. This describes the server under test (see [below for nested schema](#nestedblock--monitored_resource)) 47 | - `name` (String) Name of the check 48 | - `region` (String) Region on which to run checks 49 | 50 | ### Optional 51 | 52 | - `contact_groups` (Set of String) List of contact group IDs 53 | - `paused` (Boolean) Whether the check should be run 54 | 55 | ### Read-Only 56 | 57 | - `id` (String) The ID of this resource. 58 | - `location` (String) Assigned monitoring location on which checks will be run 59 | 60 | 61 | ### Nested Schema for `alert_config` 62 | 63 | Optional: 64 | 65 | - `alert_bigger` (Number) An alert will be sent if the size of the page is larger than this value (kb). 66 | - `alert_slower` (Number) An alert will be sent if the load time of the page exceeds this value (ms). 67 | - `alert_smaller` (Number) An alert will be sent if the size of the page is smaller than this value (kb). 68 | 69 | 70 | 71 | ### Nested Schema for `monitored_resource` 72 | 73 | Required: 74 | 75 | - `address` (String) URL or IP address of the website under test 76 | 77 | ## Import 78 | 79 | Import is supported using the following syntax: 80 | 81 | ```shell 82 | terraform import statuscake_pagespeed_check.example_com 1234 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/resources/ssl_check.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_ssl_check Resource - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_ssl_check (Resource) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "statuscake_ssl_check" "example_com" { 17 | check_interval = 600 18 | user_agent = "terraform managed SSL check" 19 | 20 | alert_config { 21 | alert_at = [7, 14, 21] 22 | on_broken = false 23 | on_expiry = true 24 | on_mixed = false 25 | on_reminder = true 26 | } 27 | 28 | contact_groups = [ 29 | statuscake_contact_group.operations_team.id 30 | ] 31 | 32 | monitored_resource { 33 | address = "https://www.example.com" 34 | } 35 | } 36 | 37 | output "example_com_ssl_check_id" { 38 | value = statuscake_ssl_check.example_com.id 39 | } 40 | ``` 41 | 42 | 43 | ## Schema 44 | 45 | ### Required 46 | 47 | - `alert_config` (Block List, Min: 1, Max: 1) Alert configuration block (see [below for nested schema](#nestedblock--alert_config)) 48 | - `check_interval` (Number) Number of seconds between checks 49 | - `monitored_resource` (Block List, Min: 1, Max: 1) Monitored resource configuration block. This describes the server under test (see [below for nested schema](#nestedblock--monitored_resource)) 50 | 51 | ### Optional 52 | 53 | - `contact_groups` (Set of String) List of contact group IDs 54 | - `follow_redirects` (Boolean) Whether to follow redirects when testing. Disabled by default 55 | - `paused` (Boolean) Whether the check should be run 56 | - `user_agent` (String) Custom user agent string set when testing 57 | 58 | ### Read-Only 59 | 60 | - `id` (String) The ID of this resource. 61 | 62 | 63 | ### Nested Schema for `alert_config` 64 | 65 | Required: 66 | 67 | - `alert_at` (Set of Number) List representing when alerts should be sent (days). Must be exactly 3 numerical values 68 | 69 | Optional: 70 | 71 | - `on_broken` (Boolean) Whether to enable alerts when SSL certificate issues are found 72 | - `on_expiry` (Boolean) Whether to enable alerts when the SSL certificate is to expire 73 | - `on_mixed` (Boolean) Whether to enable alerts when mixed content is found 74 | - `on_reminder` (Boolean) Whether to enable alert reminders 75 | 76 | 77 | 78 | ### Nested Schema for `monitored_resource` 79 | 80 | Required: 81 | 82 | - `address` (String) URL of the server under test 83 | 84 | Optional: 85 | 86 | - `hostname` (String) Hostname of the server under test 87 | 88 | ## Import 89 | 90 | Import is supported using the following syntax: 91 | 92 | ```shell 93 | terraform import statuscake_ssl_check.example_com 1234 94 | ``` 95 | -------------------------------------------------------------------------------- /internal/provider/data_source_contact_group.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/StatusCakeDev/statuscake-go" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | 11 | intvalidation "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 12 | ) 13 | 14 | func dataSourceStatusCakeContactGroup() *schema.Resource { 15 | return &schema.Resource{ 16 | ReadContext: dataSourceStatusCakeContactGroupRead, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "id": { 20 | Type: schema.TypeString, 21 | Required: true, 22 | Description: "Contact group ID", 23 | ValidateFunc: intvalidation.StringIsNumerical, 24 | }, 25 | "email_addresses": { 26 | Type: schema.TypeSet, 27 | Computed: true, 28 | Description: "List of email addresses", 29 | Elem: &schema.Schema{ 30 | Type: schema.TypeString, 31 | }, 32 | }, 33 | "integrations": { 34 | Type: schema.TypeSet, 35 | Computed: true, 36 | Description: "List of integration IDs", 37 | Elem: &schema.Schema{ 38 | Type: schema.TypeString, 39 | }, 40 | }, 41 | "mobile_numbers": { 42 | Type: schema.TypeSet, 43 | Computed: true, 44 | Description: "List of international format mobile phone numbers", 45 | Elem: &schema.Schema{ 46 | Type: schema.TypeString, 47 | }, 48 | }, 49 | "name": { 50 | Type: schema.TypeString, 51 | Computed: true, 52 | Description: "Name of the contact group", 53 | }, 54 | "ping_url": { 55 | Type: schema.TypeString, 56 | Computed: true, 57 | Description: "URL or IP address of an endpoint to push uptime events. Currently this only supports HTTP GET endpoints", 58 | }, 59 | }, 60 | } 61 | } 62 | 63 | func dataSourceStatusCakeContactGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 64 | client := meta.(*statuscake.Client) 65 | id := d.Get("id").(string) 66 | 67 | res, err := client.GetContactGroup(ctx, id).Execute() 68 | 69 | // If the resource is not found then remove it from the state. 70 | if err, ok := err.(statuscake.APIError); ok && err.Status == http.StatusNotFound { 71 | d.SetId("") 72 | return nil 73 | } 74 | if err != nil { 75 | return diag.Errorf("failed to get contact group with ID: %s", err) 76 | } 77 | 78 | if err := d.Set("email_addresses", flattenContactGroupEmailAddresses(res.Data.EmailAddresses, d)); err != nil { 79 | return diag.Errorf("failed to read email addresses: %s", err) 80 | } 81 | 82 | if err := d.Set("integrations", flattenContactGroupIntegrations(res.Data.Integrations, d)); err != nil { 83 | return diag.Errorf("failed to read integrations: %s", err) 84 | } 85 | 86 | if err := d.Set("mobile_numbers", flattenContactGroupMobileNumbers(res.Data.MobileNumbers, d)); err != nil { 87 | return diag.Errorf("failed to read mobile numbers: %s", err) 88 | } 89 | 90 | if err := d.Set("name", flattenContactGroupName(res.Data.Name, d)); err != nil { 91 | return diag.Errorf("failed to read name: %s", err) 92 | } 93 | 94 | if err := d.Set("ping_url", flattenContactGroupPingURL(res.Data.PingURL, d)); err != nil { 95 | return diag.Errorf("failed to ping url: %s", err) 96 | } 97 | 98 | d.SetId(res.Data.ID) 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behaviour that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behaviour by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behaviour and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behaviour. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviours that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 58 | reported by contacting the [project team](mailto:support@statuscake.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /.github/terraform_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/StatusCakeDev/terraform-provider-statuscake/v2 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/StatusCakeDev/statuscake-go v1.3.0 7 | github.com/hashicorp/terraform-plugin-docs v0.24.0 8 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 9 | golang.org/x/time v0.14.0 10 | ) 11 | 12 | require ( 13 | github.com/BurntSushi/toml v1.2.1 // indirect 14 | github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect 15 | github.com/Masterminds/goutils v1.1.1 // indirect 16 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 17 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 18 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 19 | github.com/agext/levenshtein v1.2.3 // indirect 20 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 21 | github.com/armon/go-radix v1.0.0 // indirect 22 | github.com/bgentry/speakeasy v0.1.0 // indirect 23 | github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect 24 | github.com/cloudflare/circl v1.6.1 // indirect 25 | github.com/fatih/color v1.16.0 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/go-cmp v0.7.0 // indirect 28 | github.com/google/uuid v1.6.0 // indirect 29 | github.com/hashicorp/cli v1.1.7 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 32 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 33 | github.com/hashicorp/go-cty v1.5.0 // indirect 34 | github.com/hashicorp/go-hclog v1.6.3 // indirect 35 | github.com/hashicorp/go-multierror v1.1.1 // indirect 36 | github.com/hashicorp/go-plugin v1.7.0 // indirect 37 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 38 | github.com/hashicorp/go-uuid v1.0.3 // indirect 39 | github.com/hashicorp/go-version v1.7.0 // indirect 40 | github.com/hashicorp/hc-install v0.9.2 // indirect 41 | github.com/hashicorp/hcl/v2 v2.24.0 // indirect 42 | github.com/hashicorp/logutils v1.0.0 // indirect 43 | github.com/hashicorp/terraform-exec v0.24.0 // indirect 44 | github.com/hashicorp/terraform-json v0.27.2 // indirect 45 | github.com/hashicorp/terraform-plugin-go v0.29.0 // indirect 46 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 47 | github.com/hashicorp/terraform-registry-address v0.4.0 // indirect 48 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 49 | github.com/hashicorp/yamux v0.1.2 // indirect 50 | github.com/huandu/xstrings v1.3.3 // indirect 51 | github.com/imdario/mergo v0.3.15 // indirect 52 | github.com/mattn/go-colorable v0.1.14 // indirect 53 | github.com/mattn/go-isatty v0.0.20 // indirect 54 | github.com/mattn/go-runewidth v0.0.9 // indirect 55 | github.com/mitchellh/copystructure v1.2.0 // indirect 56 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 57 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 58 | github.com/mitchellh/mapstructure v1.5.0 // indirect 59 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 60 | github.com/oklog/run v1.1.0 // indirect 61 | github.com/posener/complete v1.2.3 // indirect 62 | github.com/shopspring/decimal v1.3.1 // indirect 63 | github.com/spf13/cast v1.5.0 // indirect 64 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 65 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 66 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 67 | github.com/yuin/goldmark v1.7.7 // indirect 68 | github.com/yuin/goldmark-meta v1.1.0 // indirect 69 | github.com/zclconf/go-cty v1.17.0 // indirect 70 | go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect 71 | golang.org/x/crypto v0.45.0 // indirect 72 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect 73 | golang.org/x/mod v0.29.0 // indirect 74 | golang.org/x/net v0.47.0 // indirect 75 | golang.org/x/sync v0.18.0 // indirect 76 | golang.org/x/sys v0.38.0 // indirect 77 | golang.org/x/text v0.31.0 // indirect 78 | golang.org/x/tools v0.38.0 // indirect 79 | google.golang.org/appengine v1.6.8 // indirect 80 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 81 | google.golang.org/grpc v1.75.1 // indirect 82 | google.golang.org/protobuf v1.36.9 // indirect 83 | gopkg.in/yaml.v2 v2.3.0 // indirect 84 | gopkg.in/yaml.v3 v3.0.1 // indirect 85 | ) 86 | -------------------------------------------------------------------------------- /internal/provider/validation/validations_test.go: -------------------------------------------------------------------------------- 1 | package validation_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 8 | ) 9 | 10 | func TestIsEmailAddress(t *testing.T) { 11 | t.Run("returns no errors when the given value is a valid email address", func(t *testing.T) { 12 | _, errs := validation.IsEmailAddress("picard@starfleet.com", "email") 13 | if errs != nil { 14 | t.Error("expected no errors but errors were returned") 15 | } 16 | }) 17 | 18 | t.Run("returns an error when the value is not of type string", func(t *testing.T) { 19 | expected := []string{`expected type of "email" to be string`} 20 | 21 | _, errs := validation.IsEmailAddress(1701, "email") 22 | if errs == nil { 23 | t.Error("expected errors but no errors were returned") 24 | } 25 | 26 | if !reflect.DeepEqual(collect(errs), expected) { 27 | t.Error("unexpected error message") 28 | } 29 | }) 30 | 31 | t.Run("returns an error when the value is an empty string", func(t *testing.T) { 32 | expected := []string{`expected "email" email address to not be empty, got ""`} 33 | 34 | _, errs := validation.IsEmailAddress("", "email") 35 | if errs == nil { 36 | t.Error("expected errors but no errors were returned") 37 | } 38 | 39 | if !reflect.DeepEqual(collect(errs), expected) { 40 | t.Error("unexpected error message") 41 | } 42 | }) 43 | 44 | t.Run("returns an error when the value is not a valid email address", func(t *testing.T) { 45 | expected := []string{`expected "email" to be a valid email address, got "enterprise": mail: missing '@' or angle-addr`} 46 | 47 | _, errs := validation.IsEmailAddress("enterprise", "email") 48 | if errs == nil { 49 | t.Error("expected errors but no errors were returned") 50 | } 51 | 52 | if !reflect.DeepEqual(collect(errs), expected) { 53 | t.Error("unexpected error message") 54 | } 55 | }) 56 | } 57 | 58 | func TestStringIsNumerical(t *testing.T) { 59 | t.Run("returns no errors when the given value is a string representation of a numerical value", func(t *testing.T) { 60 | _, errs := validation.StringIsNumerical("123", "number") 61 | if errs != nil { 62 | t.Error("expected no errors but errors were returned") 63 | } 64 | }) 65 | 66 | t.Run("returns an error when the value is not of type string", func(t *testing.T) { 67 | expected := []string{`expected type of "number" to be string`} 68 | 69 | _, errs := validation.StringIsNumerical(1701, "number") 70 | if errs == nil { 71 | t.Error("expected errors but no errors were returned") 72 | } 73 | 74 | if !reflect.DeepEqual(collect(errs), expected) { 75 | t.Error("unexpected error message") 76 | } 77 | }) 78 | 79 | t.Run("returns an error when the value is an empty string", func(t *testing.T) { 80 | expected := []string{`expected "number" number to not be empty, got ""`} 81 | 82 | _, errs := validation.StringIsNumerical("", "number") 83 | if errs == nil { 84 | t.Error("expected errors but no errors were returned") 85 | } 86 | 87 | if !reflect.DeepEqual(collect(errs), expected) { 88 | t.Error("unexpected error message") 89 | } 90 | }) 91 | 92 | t.Run("returns an error when the value is not a string representation of a numerical value", func(t *testing.T) { 93 | expected := []string{`expected "number" to be a valid number, got "enterprise": strconv.Atoi: parsing "enterprise": invalid syntax`} 94 | 95 | _, errs := validation.StringIsNumerical("enterprise", "number") 96 | if errs == nil { 97 | t.Error("expected errors but no errors were returned") 98 | } 99 | 100 | if !reflect.DeepEqual(collect(errs), expected) { 101 | t.Error("unexpected error message") 102 | } 103 | }) 104 | } 105 | 106 | func TestInt32InSlice(t *testing.T) { 107 | t.Run("returns no errors when the given value is contained within the validation slice", func(t *testing.T) { 108 | _, errs := validation.Int32InSlice([]int32{1, 2, 3})(2, "number") 109 | if errs != nil { 110 | t.Error("expected no errors but errors were returned") 111 | } 112 | }) 113 | 114 | t.Run("returns an error when the value is not of type int32", func(t *testing.T) { 115 | expected := []string{`expected type of "number" to be integer`} 116 | 117 | _, errs := validation.Int32InSlice([]int32{1, 2, 3})("1701", "number") 118 | if errs == nil { 119 | t.Error("expected errors but no errors were returned") 120 | } 121 | 122 | if !reflect.DeepEqual(collect(errs), expected) { 123 | t.Error("unexpected error message") 124 | } 125 | }) 126 | 127 | t.Run("returns an error when the value is not contained within the validation slice", func(t *testing.T) { 128 | expected := []string{`expected "number" to be one of [1 2 3], got 4`} 129 | 130 | _, errs := validation.Int32InSlice([]int32{1, 2, 3})(4, "number") 131 | if errs == nil { 132 | t.Error("expected errors but no errors were returned") 133 | } 134 | 135 | if !reflect.DeepEqual(collect(errs), expected) { 136 | t.Error("unexpected error message") 137 | } 138 | }) 139 | } 140 | 141 | func collect(errs []error) []string { 142 | strs := make([]string, len(errs)) 143 | for i, err := range errs { 144 | strs[i] = err.Error() 145 | } 146 | return strs 147 | } 148 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "regexp" 7 | "runtime" 8 | "time" 9 | 10 | "golang.org/x/time/rate" 11 | 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 15 | 16 | "github.com/StatusCakeDev/statuscake-go" 17 | "github.com/StatusCakeDev/statuscake-go/backoff" 18 | "github.com/StatusCakeDev/statuscake-go/credentials" 19 | "github.com/StatusCakeDev/statuscake-go/throttle" 20 | ) 21 | 22 | // Provider returns a resource provider for Terraform. 23 | func Provider() *schema.Provider { 24 | return &schema.Provider{ 25 | Schema: map[string]*schema.Schema{ 26 | "api_token": { 27 | Type: schema.TypeString, 28 | Required: true, 29 | DefaultFunc: schema.EnvDefaultFunc("STATUSCAKE_API_TOKEN", nil), 30 | Description: "The API token for operations. This can also be provided as an environment variable `STATUSCAKE_API_TOKEN`", 31 | ValidateFunc: validation.StringMatch(regexp.MustCompile("[0-9a-zA-Z_]{20,30}"), "API token must only contain characters 0-9, a-zA-Z and underscores"), 32 | }, 33 | "rps": { 34 | Type: schema.TypeInt, 35 | Optional: true, 36 | DefaultFunc: schema.EnvDefaultFunc("STATUSCAKE_RPS", 4), 37 | Description: "RPS limit to apply when making calls to the API. This can also be provided as an environment variable `STATUSCAKE_RPS`", 38 | ValidateFunc: validation.IntAtLeast(1), 39 | }, 40 | "retries": { 41 | Type: schema.TypeInt, 42 | Optional: true, 43 | DefaultFunc: schema.EnvDefaultFunc("STATUSCAKE_RETRIES", 3), 44 | Description: "Maximum number of retries to perform when an API request fails. This can also be provided as an environment variable `STATUSCAKE_RETRIES`", 45 | ValidateFunc: validation.IntBetween(0, 10), 46 | }, 47 | "min_backoff": { 48 | Type: schema.TypeInt, 49 | Optional: true, 50 | DefaultFunc: schema.EnvDefaultFunc("STATUSCAKE_MIN_BACKOFF", 1), 51 | Description: "Minimum backoff period in seconds after failed API calls. This can also be provided as an environment variable `STATUSCAKE_MIN_BACKOFF`", 52 | ValidateFunc: validation.IntAtLeast(0), 53 | }, 54 | "max_backoff": { 55 | Type: schema.TypeInt, 56 | Optional: true, 57 | DefaultFunc: schema.EnvDefaultFunc("STATUSCAKE_MAX_BACKOFF", 30), 58 | Description: "Maximum backoff period in seconds after failed API calls. This can also be provided as an environment variable `STATUSCAKE_MAX_BACKOFF`", 59 | ValidateFunc: validation.IntAtLeast(1), 60 | }, 61 | "statuscake_custom_endpoint": { 62 | Type: schema.TypeString, 63 | Optional: true, 64 | DefaultFunc: schema.EnvDefaultFunc("STATUCAKE_CUSTOM_ENDPOINT", nil), 65 | Description: "Custom endpoint to which request will be made. This can also be provided as an environment variable `STATUCAKE_CUSTOM_ENDPOINT`", 66 | ValidateFunc: validation.IsURLWithHTTPorHTTPS, 67 | }, 68 | }, 69 | ResourcesMap: map[string]*schema.Resource{ 70 | "statuscake_contact_group": resourceStatusCakeContactGroup(), 71 | "statuscake_heartbeat_check": resourceStatusCakeHeartbeatCheck(), 72 | "statuscake_maintenance_window": resourceStatusCakeMaintenanceWindow(), 73 | "statuscake_pagespeed_check": resourceStatusCakePagespeedCheck(), 74 | "statuscake_ssl_check": resourceStatusCakeSSLCheck(), 75 | "statuscake_uptime_check": resourceStatusCakeUptimeCheck(), 76 | }, 77 | DataSourcesMap: map[string]*schema.Resource{ 78 | "statuscake_contact_group": dataSourceStatusCakeContactGroup(), 79 | "statuscake_pagespeed_monitoring_locations": dataSourceStatusCakeMonitoringLocations(listPagespeedMonitoringLocations), 80 | "statuscake_uptime_monitoring_locations": dataSourceStatusCakeMonitoringLocations(listUptimeMonitoringLocations), 81 | }, 82 | ConfigureContextFunc: providerConfigure, 83 | } 84 | } 85 | 86 | // providerConfigure parses the config into the Terraform provider meta object. 87 | func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { 88 | apiToken, ok := d.GetOk("api_token") 89 | if !ok { 90 | return nil, diag.Errorf("credentials are not set correctly") 91 | } 92 | 93 | bearer := credentials.NewBearerWithStaticToken(apiToken.(string)) 94 | opts := []statuscake.Option{ 95 | statuscake.WithBackoff(backoff.Exponential{ 96 | BaseDelay: time.Duration(d.Get("min_backoff").(int)) * time.Second, 97 | Multiplier: 2.0, 98 | Jitter: 0.2, 99 | MaxDelay: time.Duration(d.Get("max_backoff").(int)) * time.Second, 100 | }), 101 | statuscake.WithHTTPClient(&http.Client{ 102 | Transport: throttle.NewWithDefaultTransport( 103 | rate.NewLimiter(rate.Limit(d.Get("rps").(int)), 1), 104 | ), 105 | }), 106 | statuscake.WithMaxRetries(d.Get("retries").(int)), 107 | statuscake.WithRequestCredentials(bearer), 108 | statuscake.WithUserAgent("terraform-provider-statuscake/" + runtime.Version()), 109 | } 110 | 111 | if customEndpoint, ok := d.GetOk("statuscake_custom_endpoint"); ok { 112 | opts = append(opts, statuscake.WithHost(customEndpoint.(string))) 113 | } 114 | 115 | return statuscake.NewClient(opts...), nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/provider/data_source_monitoring_locations.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/StatusCakeDev/statuscake-go" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 12 | ) 13 | 14 | type monitoringLocationsFunc func(context.Context, *statuscake.Client, string) (statuscake.MonitoringLocations, error) 15 | 16 | func dataSourceStatusCakeMonitoringLocations(fn monitoringLocationsFunc) *schema.Resource { 17 | return &schema.Resource{ 18 | ReadContext: dataSourceStatusCakeMonitoringLocationsRead(fn), 19 | 20 | Schema: map[string]*schema.Schema{ 21 | "region_code": { 22 | Type: schema.TypeString, 23 | Optional: true, 24 | Description: "Location region code", 25 | ValidateFunc: validation.StringIsNotEmpty, 26 | }, 27 | "locations": { 28 | Type: schema.TypeList, 29 | Computed: true, 30 | Description: "List of monitoring locations", 31 | Elem: &schema.Resource{ 32 | Schema: locationSchema(), 33 | }, 34 | }, 35 | }, 36 | } 37 | } 38 | 39 | // locationsSchema returns the schema describing a single monitoring locations. 40 | // Since locations features within multiple resources its structure has been 41 | // encapsulated within a function. 42 | func locationSchema() map[string]*schema.Schema { 43 | return map[string]*schema.Schema{ 44 | "description": { 45 | Type: schema.TypeString, 46 | Computed: true, 47 | Description: "Location description", 48 | }, 49 | "ipv4": { 50 | Type: schema.TypeString, 51 | Computed: true, 52 | Description: "Location IPv4 address", 53 | }, 54 | "ipv6": { 55 | Type: schema.TypeString, 56 | Computed: true, 57 | Description: "Location IPv6 address", 58 | }, 59 | "region": { 60 | Type: schema.TypeString, 61 | Computed: true, 62 | Description: "Location region", 63 | }, 64 | "region_code": { 65 | Type: schema.TypeString, 66 | Computed: true, 67 | Description: "Location region code", 68 | }, 69 | "status": { 70 | Type: schema.TypeString, 71 | Computed: true, 72 | Description: "Location status", 73 | }, 74 | } 75 | } 76 | 77 | func dataSourceStatusCakeMonitoringLocationsRead(fn monitoringLocationsFunc) schema.ReadContextFunc { 78 | return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 79 | client := meta.(*statuscake.Client) 80 | 81 | res, err := fn(ctx, client, d.Get("region_code").(string)) 82 | if err != nil { 83 | return diag.Errorf("failed to list monitoring locations: %s", err) 84 | } 85 | 86 | if err := d.Set("locations", flattenMonitoringLocations(res.Data, d)); err != nil { 87 | return diag.Errorf("error setting monitoring locations: %s", err) 88 | } 89 | 90 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 91 | 92 | return nil 93 | } 94 | } 95 | 96 | func listUptimeMonitoringLocations(ctx context.Context, client *statuscake.Client, regionCode string) (statuscake.MonitoringLocations, error) { 97 | req := client.ListUptimeMonitoringLocations(ctx) 98 | 99 | if len(regionCode) != 0 { 100 | req = req.RegionCode(regionCode) 101 | } 102 | 103 | return req.Execute() 104 | } 105 | 106 | func listPagespeedMonitoringLocations(ctx context.Context, client *statuscake.Client, location string) (statuscake.MonitoringLocations, error) { 107 | req := client.ListPagespeedMonitoringLocations(ctx) 108 | 109 | if len(location) != 0 { 110 | req = req.Location(location) 111 | } 112 | 113 | return req.Execute() 114 | } 115 | 116 | func flattenMonitoringLocationsDescription(v interface{}, d *schema.ResourceData) interface{} { 117 | return v 118 | } 119 | 120 | func flattenMonitoringLocationsIPv4(v interface{}, d *schema.ResourceData) interface{} { 121 | return v 122 | } 123 | 124 | func flattenMonitoringLocationsIPv6(v interface{}, d *schema.ResourceData) interface{} { 125 | return v 126 | } 127 | 128 | func flattenMonitoringLocations(v interface{}, d *schema.ResourceData) interface{} { 129 | data := v.([]statuscake.MonitoringLocation) 130 | 131 | locations := make([]interface{}, len(data)) 132 | for idx, location := range data { 133 | locations[idx] = flattenMonitoringLocation(location, d) 134 | } 135 | 136 | return locations 137 | } 138 | 139 | func flattenMonitoringLocation(v interface{}, d *schema.ResourceData) interface{} { 140 | data := v.(statuscake.MonitoringLocation) 141 | 142 | return map[string]interface{}{ 143 | "description": flattenMonitoringLocationsDescription(data.Description, d), 144 | "ipv4": flattenMonitoringLocationsIPv4(data.IPv4, d), 145 | "ipv6": flattenMonitoringLocationsIPv6(data.IPv6, d), 146 | "region": flattenMonitoringLocationsRegion(data.Region, d), 147 | "region_code": flattenMonitoringLocationsRegionCode(data.RegionCode, d), 148 | "status": flattenMonitoringLocationsStatus(data.Status, d), 149 | } 150 | } 151 | 152 | func flattenMonitoringLocationsRegion(v interface{}, d *schema.ResourceData) interface{} { 153 | return v 154 | } 155 | 156 | func flattenMonitoringLocationsRegionCode(v interface{}, d *schema.ResourceData) interface{} { 157 | return v 158 | } 159 | 160 | func flattenMonitoringLocationsStatus(v interface{}, d *schema.ResourceData) interface{} { 161 | return v 162 | } 163 | -------------------------------------------------------------------------------- /docs/resources/uptime_check.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "statuscake_uptime_check Resource - terraform-provider-statuscake" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # statuscake_uptime_check (Resource) 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "statuscake_uptime_check" "example_com" { 17 | check_interval = 30 18 | confirmation = 3 19 | name = "Example" 20 | trigger_rate = 10 21 | 22 | contact_groups = [ 23 | statuscake_contact_group.operations_team.id 24 | ] 25 | 26 | http_check { 27 | enable_cookies = true 28 | follow_redirects = true 29 | timeout = 20 30 | user_agent = "terraform managed uptime check" 31 | validate_ssl = true 32 | 33 | basic_authentication { 34 | password = "password" 35 | username = "username" 36 | } 37 | 38 | content_matchers { 39 | content = "Welcome" 40 | include_headers = true 41 | matcher = "CONTAINS_STRING" 42 | } 43 | 44 | request_headers = { 45 | Authorization = "bearer 123456" 46 | } 47 | 48 | status_codes = [ 49 | "202", 50 | "404", 51 | "405", 52 | ] 53 | } 54 | 55 | monitored_resource { 56 | address = "https://www.example.com" 57 | } 58 | 59 | regions = [ 60 | "london", 61 | "london", 62 | "paris", 63 | ] 64 | 65 | tags = [ 66 | "production", 67 | ] 68 | } 69 | 70 | output "example_com_uptime_check_id" { 71 | value = statuscake_uptime_check.example_com.id 72 | } 73 | ``` 74 | 75 | 76 | ## Schema 77 | 78 | ### Required 79 | 80 | - `check_interval` (Number) Number of seconds between checks 81 | - `monitored_resource` (Block List, Min: 1, Max: 1) Monitored resource configuration block. This describes the server under test (see [below for nested schema](#nestedblock--monitored_resource)) 82 | - `name` (String) Name of the check 83 | 84 | ### Optional 85 | 86 | - `confirmation` (Number) Number of confirmation servers to confirm downtime before an alert is triggered 87 | - `contact_groups` (Set of String) List of contact group IDs 88 | - `dns_check` (Block List, Max: 1) DNS check configuration block. Only one of `dns_check`, `http_check`, `icmp_check`, and `tcp_check` may be specified (see [below for nested schema](#nestedblock--dns_check)) 89 | - `http_check` (Block List, Max: 1) HTTP check configuration block. Only one of `dns_check`, `http_check`, `icmp_check`, and `tcp_check` may be specified (see [below for nested schema](#nestedblock--http_check)) 90 | - `icmp_check` (Block List, Max: 1) ICMP check configuration block. Only one of `dns_check`, `http_check`, `icmp_check`, and `tcp_check` may be specified (see [below for nested schema](#nestedblock--icmp_check)) 91 | - `paused` (Boolean) Whether the check should be run 92 | - `regions` (List of String) List of regions on which to run checks. The values required for this parameter can be retrieved from the `GET /v1/uptime-locations` endpoint 93 | - `tags` (Set of String) List of tags 94 | - `tcp_check` (Block List, Max: 1) TCP check configuration block. Only one of `dns_check`, `http_check`, `icmp_check`, and `tcp_check` may be specified (see [below for nested schema](#nestedblock--tcp_check)) 95 | - `trigger_rate` (Number) The number of minutes to wait before sending an alert 96 | 97 | ### Read-Only 98 | 99 | - `id` (String) The ID of this resource. 100 | - `locations` (Set of Object) List of assigned monitoring locations on which to run checks (see [below for nested schema](#nestedatt--locations)) 101 | 102 | 103 | ### Nested Schema for `monitored_resource` 104 | 105 | Required: 106 | 107 | - `address` (String) URL, FQDN, or IP address of the server under test 108 | 109 | Optional: 110 | 111 | - `host` (String) Name of the hosting provider 112 | 113 | 114 | 115 | ### Nested Schema for `dns_check` 116 | 117 | Required: 118 | 119 | - `dns_ips` (Set of String) List of IP addresses to compare against returned DNS records 120 | 121 | Optional: 122 | 123 | - `dns_server` (String) FQDN or IP address of the nameserver to query 124 | 125 | 126 | 127 | ### Nested Schema for `http_check` 128 | 129 | Optional: 130 | 131 | - `basic_authentication` (Block List, Max: 1) Basic Authentication (RFC7235) configuration block (see [below for nested schema](#nestedblock--http_check--basic_authentication)) 132 | - `content_matchers` (Block List, Max: 1) Content matcher configuration block. This is used to assert values within the response of the request (see [below for nested schema](#nestedblock--http_check--content_matchers)) 133 | - `enable_cookies` (Boolean) Whether to enable cookie storage 134 | - `final_endpoint` (String) Specify where the redirect chain should end 135 | - `follow_redirects` (Boolean) Whether to follow redirects when testing. Disabled by default 136 | - `request_headers` (Map of String) Represents headers to be sent when making requests 137 | - `request_method` (String) Type of HTTP check. Either HTTP, or HEAD 138 | - `request_payload` (Map of String) Payload submitted with the request. Setting this updates the check to use the HTTP POST verb. Only one of `request_payload` or `request_payload_raw` may be specified 139 | - `request_payload_raw` (String) Raw payload submitted with the request. Setting this updates the check to use the HTTP POST verb. Only one of `request_payload` or `request_payload_raw` may be specified 140 | - `status_codes` (Set of String) List of status codes that trigger an alert. If not specified then the default status codes are used. Once set, the default status codes cannot be restored and ommitting this field does not clear the attribute 141 | - `timeout` (Number) The number of seconds to wait to receive the first byte 142 | - `user_agent` (String) Custom user agent string set when testing 143 | - `validate_ssl` (Boolean) Whether to send an alert if the SSL certificate is soon to expire 144 | 145 | 146 | ### Nested Schema for `http_check.basic_authentication` 147 | 148 | Required: 149 | 150 | - `password` (String, Sensitive) 151 | - `username` (String) 152 | 153 | 154 | 155 | ### Nested Schema for `http_check.content_matchers` 156 | 157 | Required: 158 | 159 | - `content` (String) String to look for within the response. Considered down if not found 160 | 161 | Optional: 162 | 163 | - `include_headers` (Boolean) Include header content in string match search 164 | - `matcher` (String) Whether to consider the check as down if the content is present within the response 165 | 166 | 167 | 168 | 169 | ### Nested Schema for `icmp_check` 170 | 171 | Optional: 172 | 173 | - `enabled` (Boolean) Dummy attribute to allow for a nested block. This field should not be changed 174 | 175 | 176 | 177 | ### Nested Schema for `tcp_check` 178 | 179 | Required: 180 | 181 | - `port` (Number) Destination port for TCP checks 182 | 183 | Optional: 184 | 185 | - `authentication` (Block List, Max: 1) Authentication configuration block (see [below for nested schema](#nestedblock--tcp_check--authentication)) 186 | - `protocol` (String) Type of TCP check. Either SMTP, SSH or TCP 187 | - `timeout` (Number) The number of seconds to wait to receive the first byte 188 | 189 | 190 | ### Nested Schema for `tcp_check.authentication` 191 | 192 | Required: 193 | 194 | - `password` (String, Sensitive) 195 | - `username` (String) 196 | 197 | 198 | 199 | 200 | ### Nested Schema for `locations` 201 | 202 | Read-Only: 203 | 204 | - `description` (String) 205 | - `ipv4` (String) 206 | - `ipv6` (String) 207 | - `region` (String) 208 | - `region_code` (String) 209 | - `status` (String) 210 | 211 | ## Import 212 | 213 | Import is supported using the following syntax: 214 | 215 | ```shell 216 | terraform import statuscake_uptime_check.example_com 1234 217 | ``` 218 | -------------------------------------------------------------------------------- /internal/provider/resource_contact_group.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/StatusCakeDev/statuscake-go" 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 | intdiag "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/diag" 15 | intvalidation "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 16 | ) 17 | 18 | func resourceStatusCakeContactGroup() *schema.Resource { 19 | return &schema.Resource{ 20 | CreateContext: resourceStatusCakeContactGroupCreate, 21 | ReadContext: resourceStatusCakeContactGroupRead, 22 | UpdateContext: resourceStatusCakeContactGroupUpdate, 23 | DeleteContext: resourceStatusCakeContactGroupDelete, 24 | 25 | // Used by `terraform import`. 26 | Importer: &schema.ResourceImporter{ 27 | StateContext: schema.ImportStatePassthroughContext, 28 | }, 29 | 30 | Schema: map[string]*schema.Schema{ 31 | "email_addresses": { 32 | Type: schema.TypeSet, 33 | Optional: true, 34 | Description: "List of email addresses", 35 | Elem: &schema.Schema{ 36 | Type: schema.TypeString, 37 | ValidateFunc: intvalidation.IsEmailAddress, 38 | }, 39 | }, 40 | "integrations": { 41 | Type: schema.TypeSet, 42 | Optional: true, 43 | Description: "List of integration IDs", 44 | Elem: &schema.Schema{ 45 | Type: schema.TypeString, 46 | ValidateFunc: intvalidation.StringIsNumerical, 47 | }, 48 | }, 49 | "mobile_numbers": { 50 | Type: schema.TypeSet, 51 | Optional: true, 52 | Description: "List of international format mobile phone numbers", 53 | Elem: &schema.Schema{ 54 | Type: schema.TypeString, 55 | ValidateFunc: validation.StringIsNotEmpty, 56 | }, 57 | }, 58 | "name": { 59 | Type: schema.TypeString, 60 | Required: true, 61 | Description: "Name of the contact group", 62 | ValidateFunc: validation.StringIsNotEmpty, 63 | }, 64 | "ping_url": { 65 | Type: schema.TypeString, 66 | Optional: true, 67 | Description: "URL or IP address of an endpoint to push uptime events. Currently this only supports HTTP GET endpoints", 68 | ValidateFunc: validation.IsURLWithHTTPorHTTPS, 69 | }, 70 | }, 71 | } 72 | } 73 | 74 | func resourceStatusCakeContactGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 75 | client := meta.(*statuscake.Client) 76 | body := make(map[string]interface{}) 77 | 78 | emailAddresses, err := expandContactGroupEmailAddresses(d.Get("email_addresses"), d) 79 | if err != nil { 80 | return diag.FromErr(err) 81 | } else if d.HasChange("email_addresses") { 82 | body["email_addresses"] = emailAddresses 83 | } 84 | 85 | integrations, err := expandContactGroupIntegrations(d.Get("integrations"), d) 86 | if err != nil { 87 | return diag.FromErr(err) 88 | } else if d.HasChange("integrations") { 89 | body["integrations"] = integrations 90 | } 91 | 92 | mobileNumbers, err := expandContactGroupMobileNumbers(d.Get("mobile_numbers"), d) 93 | if err != nil { 94 | return diag.FromErr(err) 95 | } else if d.HasChange("mobile_numbers") { 96 | body["mobile_numbers"] = mobileNumbers 97 | } 98 | 99 | name, err := expandContactGroupName(d.Get("name"), d) 100 | if err != nil { 101 | return diag.FromErr(err) 102 | } else if d.HasChange("name") { 103 | body["name"] = name 104 | } 105 | 106 | url, err := expandContactGroupPingURL(d.Get("ping_url"), d) 107 | if err != nil { 108 | return diag.FromErr(err) 109 | } else if d.HasChange("ping_url") { 110 | body["ping_url"] = url 111 | } 112 | 113 | log.Print("[DEBUG] Creating StatusCake contact group") 114 | log.Printf("[DEBUG] Request body: %+v", body) 115 | 116 | res, err := client.CreateContactGroupWithData(ctx, body).Execute() 117 | if err != nil { 118 | return intdiag.FromErr("failed to create contact group", err) 119 | } 120 | 121 | d.SetId(res.Data.NewID) 122 | return resourceStatusCakeContactGroupRead(ctx, d, meta) 123 | } 124 | 125 | func resourceStatusCakeContactGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 126 | client := meta.(*statuscake.Client) 127 | id := d.Id() 128 | 129 | res, err := client.GetContactGroup(ctx, id).Execute() 130 | 131 | // If the resource is not found then remove it from the state. 132 | if err, ok := err.(statuscake.APIError); ok && err.Status == http.StatusNotFound { 133 | d.SetId("") 134 | return nil 135 | } 136 | if err != nil { 137 | return diag.Errorf("failed to get contact group with ID: %s, error: %s", id, err) 138 | } 139 | 140 | if err := d.Set("email_addresses", flattenContactGroupEmailAddresses(res.Data.EmailAddresses, d)); err != nil { 141 | return diag.Errorf("failed to read email addresses: %s", err) 142 | } 143 | 144 | if err := d.Set("integrations", flattenContactGroupIntegrations(res.Data.Integrations, d)); err != nil { 145 | return diag.Errorf("failed to read integrations: %s", err) 146 | } 147 | 148 | if err := d.Set("mobile_numbers", flattenContactGroupMobileNumbers(res.Data.MobileNumbers, d)); err != nil { 149 | return diag.Errorf("failed to read mobile numbers: %s", err) 150 | } 151 | 152 | if err := d.Set("name", flattenContactGroupName(res.Data.Name, d)); err != nil { 153 | return diag.Errorf("failed to read name: %s", err) 154 | } 155 | 156 | if err := d.Set("ping_url", flattenContactGroupPingURL(res.Data.PingURL, d)); err != nil { 157 | return diag.Errorf("failed to ping url: %s", err) 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func resourceStatusCakeContactGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 164 | client := meta.(*statuscake.Client) 165 | body := make(map[string]interface{}) 166 | id := d.Id() 167 | 168 | emailAddresses, err := expandContactGroupEmailAddresses(d.Get("email_addresses"), d) 169 | if err != nil { 170 | return diag.FromErr(err) 171 | } else if d.HasChange("email_addresses") { 172 | body["email_addresses"] = emailAddresses 173 | } 174 | 175 | integrations, err := expandContactGroupIntegrations(d.Get("integrations"), d) 176 | if err != nil { 177 | return diag.FromErr(err) 178 | } else if d.HasChange("integrations") { 179 | body["integrations"] = integrations 180 | } 181 | 182 | mobileNumbers, err := expandContactGroupMobileNumbers(d.Get("mobile_numbers"), d) 183 | if err != nil { 184 | return diag.FromErr(err) 185 | } else if d.HasChange("mobile_numbers") { 186 | body["mobile_numbers"] = mobileNumbers 187 | } 188 | 189 | name, err := expandContactGroupName(d.Get("name"), d) 190 | if err != nil { 191 | return diag.FromErr(err) 192 | } else if d.HasChange("name") { 193 | body["name"] = name 194 | } 195 | 196 | url, err := expandContactGroupPingURL(d.Get("ping_url"), d) 197 | if err != nil { 198 | return diag.FromErr(err) 199 | } else if d.HasChange("ping_url") { 200 | body["ping_url"] = url 201 | } 202 | 203 | log.Printf("[DEBUG] Updating StatusCake contact group with ID: %s", id) 204 | log.Printf("[DEBUG] Request body: %+v", body) 205 | 206 | if err := client.UpdateContactGroupWithData(ctx, id, body).Execute(); err != nil { 207 | return intdiag.FromErr(fmt.Sprintf("failed to update contact group with id %s", id), err) 208 | } 209 | 210 | return resourceStatusCakeContactGroupRead(ctx, d, meta) 211 | } 212 | 213 | func resourceStatusCakeContactGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 214 | client := meta.(*statuscake.Client) 215 | id := d.Id() 216 | 217 | log.Printf("[DEBUG] Deleting StatusCake contact group with ID: %s", id) 218 | 219 | if err := client.DeleteContactGroup(ctx, id).Execute(); err != nil { 220 | return intdiag.FromErr(fmt.Sprintf("failed to delete contact group with id %s", id), err) 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func expandContactGroupEmailAddresses(v interface{}, d *schema.ResourceData) (interface{}, error) { 227 | return convertStringSet(v.(*schema.Set)), nil 228 | } 229 | 230 | func flattenContactGroupEmailAddresses(v interface{}, d *schema.ResourceData) interface{} { 231 | return v 232 | } 233 | 234 | func expandContactGroupIntegrations(v interface{}, d *schema.ResourceData) (interface{}, error) { 235 | return convertStringSet(v.(*schema.Set)), nil 236 | } 237 | 238 | func flattenContactGroupIntegrations(v interface{}, d *schema.ResourceData) interface{} { 239 | return v 240 | } 241 | 242 | func expandContactGroupMobileNumbers(v interface{}, d *schema.ResourceData) (interface{}, error) { 243 | return convertStringSet(v.(*schema.Set)), nil 244 | } 245 | 246 | func flattenContactGroupMobileNumbers(v interface{}, d *schema.ResourceData) interface{} { 247 | return v 248 | } 249 | 250 | func expandContactGroupPingURL(v interface{}, d *schema.ResourceData) (interface{}, error) { 251 | return v.(string), nil 252 | } 253 | 254 | func flattenContactGroupPingURL(v interface{}, d *schema.ResourceData) interface{} { 255 | return v 256 | } 257 | 258 | func expandContactGroupName(v interface{}, d *schema.ResourceData) (interface{}, error) { 259 | return v.(string), nil 260 | } 261 | 262 | func flattenContactGroupName(v interface{}, d *schema.ResourceData) interface{} { 263 | return v 264 | } 265 | -------------------------------------------------------------------------------- /internal/provider/resource_maintenance_window.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/StatusCakeDev/statuscake-go" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 14 | 15 | intdiag "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/diag" 16 | intvalidation "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 17 | ) 18 | 19 | func resourceStatusCakeMaintenanceWindow() *schema.Resource { 20 | return &schema.Resource{ 21 | CreateContext: resourceStatusCakeMaintenanceWindowCreate, 22 | ReadContext: resourceStatusCakeMaintenanceWindowRead, 23 | UpdateContext: resourceStatusCakeMaintenanceWindowUpdate, 24 | DeleteContext: resourceStatusCakeMaintenanceWindowDelete, 25 | 26 | // Used by `terraform import`. 27 | Importer: &schema.ResourceImporter{ 28 | StateContext: schema.ImportStatePassthroughContext, 29 | }, 30 | 31 | Schema: map[string]*schema.Schema{ 32 | "end": { 33 | Type: schema.TypeString, 34 | Required: true, 35 | Description: "End of the maintenance window (RFC3339 format)", 36 | ValidateFunc: validation.IsRFC3339Time, 37 | }, 38 | "name": { 39 | Type: schema.TypeString, 40 | Required: true, 41 | Description: "Name of the maintenance window", 42 | ValidateFunc: validation.StringIsNotEmpty, 43 | }, 44 | "repeat_interval": { 45 | Type: schema.TypeString, 46 | Optional: true, 47 | Default: "never", 48 | Description: "How often the maintenance window should occur", 49 | ValidateFunc: validation.StringInSlice(statuscake.MaintenanceWindowRepeatIntervalValues(), false), 50 | }, 51 | "start": { 52 | Type: schema.TypeString, 53 | Required: true, 54 | Description: "Start of the maintenance window (RFC3339 format)", 55 | ValidateFunc: validation.IsRFC3339Time, 56 | }, 57 | "tags": { 58 | Type: schema.TypeSet, 59 | Optional: true, 60 | Description: "List of tags used to include matching uptime checks in this maintenance window", 61 | Elem: &schema.Schema{ 62 | Type: schema.TypeString, 63 | ValidateFunc: validation.StringIsNotEmpty, 64 | }, 65 | AtLeastOneOf: []string{"tags", "tests"}, 66 | }, 67 | "tests": { 68 | Type: schema.TypeSet, 69 | Optional: true, 70 | Description: "List of uptime check IDs explicitly included in this maintenance window", 71 | Elem: &schema.Schema{ 72 | Type: schema.TypeString, 73 | ValidateFunc: intvalidation.StringIsNumerical, 74 | }, 75 | AtLeastOneOf: []string{"tags", "tests"}, 76 | }, 77 | "timezone": { 78 | Type: schema.TypeString, 79 | Required: true, 80 | Description: "Standard timezone associated with this maintenance window", 81 | ValidateFunc: validation.StringIsNotEmpty, 82 | }, 83 | }, 84 | } 85 | } 86 | 87 | func resourceStatusCakeMaintenanceWindowCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 88 | client := meta.(*statuscake.Client) 89 | body := make(map[string]interface{}) 90 | 91 | end, err := expandMaintenanceWindowEnd(d.Get("end"), d) 92 | if err != nil { 93 | return diag.FromErr(err) 94 | } else if d.HasChange("end") { 95 | body["end_at"] = end 96 | } 97 | 98 | name, err := expandMaintenanceWindowName(d.Get("name"), d) 99 | if err != nil { 100 | return diag.FromErr(err) 101 | } else if d.HasChange("name") { 102 | body["name"] = name 103 | } 104 | 105 | interval, err := expandMaintenanceWindowRepeatInterval(d.Get("repeat_interval"), d) 106 | if err != nil { 107 | return diag.FromErr(err) 108 | } else if d.HasChange("repeat_interval") { 109 | body["repeat_interval"] = interval 110 | } 111 | 112 | start, err := expandMaintenanceWindowStart(d.Get("start"), d) 113 | if err != nil { 114 | return diag.FromErr(err) 115 | } else if d.HasChange("start") { 116 | body["start_at"] = start 117 | } 118 | 119 | tags, err := expandMaintenanceWindowTags(d.Get("tags"), d) 120 | if err != nil { 121 | return diag.FromErr(err) 122 | } else if d.HasChange("tags") { 123 | body["tags"] = tags 124 | } 125 | 126 | tests, err := expandMaintenanceWindowTests(d.Get("tests"), d) 127 | if err != nil { 128 | return diag.FromErr(err) 129 | } else if d.HasChange("tests") { 130 | body["tests"] = tests 131 | } 132 | 133 | timezone, err := expandMaintenanceWindowTimezone(d.Get("timezone"), d) 134 | if err != nil { 135 | return diag.FromErr(err) 136 | } else if d.HasChange("timezone") { 137 | body["timezone"] = timezone 138 | } 139 | 140 | log.Print("[DEBUG] Creating StatusCake maintenance window") 141 | log.Printf("[DEBUG] Request body: %+v", body) 142 | 143 | res, err := client.CreateMaintenanceWindowWithData(ctx, body).Execute() 144 | if err != nil { 145 | return intdiag.FromErr("failed to create maintenance window", err) 146 | } 147 | 148 | d.SetId(res.Data.NewID) 149 | return resourceStatusCakeMaintenanceWindowRead(ctx, d, meta) 150 | } 151 | 152 | func resourceStatusCakeMaintenanceWindowRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 153 | client := meta.(*statuscake.Client) 154 | id := d.Id() 155 | 156 | res, err := client.GetMaintenanceWindow(ctx, id).Execute() 157 | 158 | // If the resource is not found then remove it from the state. 159 | if err, ok := err.(statuscake.APIError); ok && err.Status == http.StatusNotFound { 160 | d.SetId("") 161 | return nil 162 | } 163 | if err != nil { 164 | return diag.Errorf("failed to get maintenance window test with ID: %s, error: %s", id, err) 165 | } 166 | 167 | if err := d.Set("end", flattenMaintenanceWindowEnd(res.Data.End, d)); err != nil { 168 | return diag.Errorf("failed to read end: %s", err) 169 | } 170 | 171 | if err := d.Set("name", flattenMaintenanceWindowName(res.Data.Name, d)); err != nil { 172 | return diag.Errorf("failed to read name: %s", err) 173 | } 174 | 175 | if err := d.Set("repeat_interval", flattenMaintenanceWindowRepeatInterval(res.Data.RepeatInterval, d)); err != nil { 176 | return diag.Errorf("failed to read repeat interval: %s", err) 177 | } 178 | 179 | if err := d.Set("start", flattenMaintenanceWindowStart(res.Data.Start, d)); err != nil { 180 | return diag.Errorf("failed to read start: %s", err) 181 | } 182 | 183 | if err := d.Set("tags", flattenMaintenanceWindowTags(res.Data.Tags, d)); err != nil { 184 | return diag.Errorf("failed to read tags: %s", err) 185 | } 186 | 187 | if err := d.Set("tests", flattenMaintenanceWindowTests(res.Data.Tests, d)); err != nil { 188 | return diag.Errorf("failed to read tests: %s", err) 189 | } 190 | 191 | if err := d.Set("timezone", flattenMaintenanceWindowTimezone(res.Data.Timezone, d)); err != nil { 192 | return diag.Errorf("failed to read timezone: %s", err) 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func resourceStatusCakeMaintenanceWindowUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 199 | client := meta.(*statuscake.Client) 200 | body := make(map[string]interface{}) 201 | id := d.Id() 202 | 203 | end, err := expandMaintenanceWindowEnd(d.Get("end"), d) 204 | if err != nil { 205 | return diag.FromErr(err) 206 | } else if d.HasChange("end") { 207 | body["end_at"] = end 208 | } 209 | 210 | name, err := expandMaintenanceWindowName(d.Get("name"), d) 211 | if err != nil { 212 | return diag.FromErr(err) 213 | } else if d.HasChange("name") { 214 | body["name"] = name 215 | } 216 | 217 | interval, err := expandMaintenanceWindowRepeatInterval(d.Get("repeat_interval"), d) 218 | if err != nil { 219 | return diag.FromErr(err) 220 | } else if d.HasChange("repeat_interval") { 221 | body["repeat_interval"] = interval 222 | } 223 | 224 | start, err := expandMaintenanceWindowStart(d.Get("start"), d) 225 | if err != nil { 226 | return diag.FromErr(err) 227 | } else if d.HasChange("start") { 228 | body["start_at"] = start 229 | } 230 | 231 | tags, err := expandMaintenanceWindowTags(d.Get("tags"), d) 232 | if err != nil { 233 | return diag.FromErr(err) 234 | } else if d.HasChange("tags") { 235 | body["tags"] = tags 236 | } 237 | 238 | tests, err := expandMaintenanceWindowTests(d.Get("tests"), d) 239 | if err != nil { 240 | return diag.FromErr(err) 241 | } else if d.HasChange("tests") { 242 | body["tests"] = tests 243 | } 244 | 245 | timezone, err := expandMaintenanceWindowTimezone(d.Get("timezone"), d) 246 | if err != nil { 247 | return diag.FromErr(err) 248 | } else if d.HasChange("timezone") { 249 | body["timezone"] = timezone 250 | } 251 | 252 | log.Printf("[DEBUG] Updating StatusCake maintenance window with ID: %s", id) 253 | log.Printf("[DEBUG] Request body: %+v", body) 254 | 255 | if err := client.UpdateMaintenanceWindowWithData(ctx, id, body).Execute(); err != nil { 256 | return intdiag.FromErr(fmt.Sprintf("failed to update maintenance window with id %s", id), err) 257 | } 258 | 259 | return resourceStatusCakeMaintenanceWindowRead(ctx, d, meta) 260 | } 261 | 262 | func resourceStatusCakeMaintenanceWindowDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 263 | client := meta.(*statuscake.Client) 264 | id := d.Id() 265 | 266 | log.Printf("[DEBUG] Deleting StatusCake maintenance window with ID: %s", id) 267 | 268 | if err := client.DeleteMaintenanceWindow(ctx, id).Execute(); err != nil { 269 | return intdiag.FromErr(fmt.Sprintf("failed to delete maintenance window with id %s", id), err) 270 | } 271 | 272 | return nil 273 | } 274 | 275 | func expandMaintenanceWindowEnd(v interface{}, d *schema.ResourceData) (interface{}, error) { 276 | return time.Parse(time.RFC3339, v.(string)) 277 | } 278 | 279 | func flattenMaintenanceWindowEnd(v interface{}, _ *schema.ResourceData) interface{} { 280 | t := v.(time.Time) 281 | return t.Format(time.RFC3339) 282 | } 283 | 284 | func expandMaintenanceWindowName(v interface{}, d *schema.ResourceData) (interface{}, error) { 285 | return v.(string), nil 286 | } 287 | 288 | func flattenMaintenanceWindowName(v interface{}, d *schema.ResourceData) interface{} { 289 | return v 290 | } 291 | 292 | func expandMaintenanceWindowRepeatInterval(v interface{}, d *schema.ResourceData) (interface{}, error) { 293 | return statuscake.MaintenanceWindowRepeatInterval(v.(string)), nil 294 | } 295 | 296 | func flattenMaintenanceWindowRepeatInterval(v interface{}, d *schema.ResourceData) interface{} { 297 | return v 298 | } 299 | 300 | func expandMaintenanceWindowStart(v interface{}, d *schema.ResourceData) (interface{}, error) { 301 | return time.Parse(time.RFC3339, v.(string)) 302 | } 303 | 304 | func flattenMaintenanceWindowStart(v interface{}, _ *schema.ResourceData) interface{} { 305 | t := v.(time.Time) 306 | return t.Format(time.RFC3339) 307 | } 308 | 309 | func expandMaintenanceWindowTags(v interface{}, d *schema.ResourceData) (interface{}, error) { 310 | return convertStringSet(v.(*schema.Set)), nil 311 | } 312 | 313 | func flattenMaintenanceWindowTags(v interface{}, d *schema.ResourceData) interface{} { 314 | return v 315 | } 316 | 317 | func expandMaintenanceWindowTests(v interface{}, d *schema.ResourceData) (interface{}, error) { 318 | return convertStringSet(v.(*schema.Set)), nil 319 | } 320 | 321 | func flattenMaintenanceWindowTests(v interface{}, d *schema.ResourceData) interface{} { 322 | return v 323 | } 324 | 325 | func expandMaintenanceWindowTimezone(v interface{}, d *schema.ResourceData) (interface{}, error) { 326 | return v.(string), nil 327 | } 328 | 329 | func flattenMaintenanceWindowTimezone(v interface{}, d *schema.ResourceData) interface{} { 330 | return v 331 | } 332 | -------------------------------------------------------------------------------- /internal/provider/resource_heartbeat_check.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/StatusCakeDev/statuscake-go" 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 | intdiag "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/diag" 15 | intvalidation "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 16 | ) 17 | 18 | func resourceStatusCakeHeartbeatCheck() *schema.Resource { 19 | return &schema.Resource{ 20 | CreateContext: resourceStatusCakeHeartbeatCheckCreate, 21 | ReadContext: resourceStatusCakeHeartbeatCheckRead, 22 | UpdateContext: resourceStatusCakeHeartbeatCheckUpdate, 23 | DeleteContext: resourceStatusCakeHeartbeatCheckDelete, 24 | 25 | // Used by `terraform import`. 26 | Importer: &schema.ResourceImporter{ 27 | StateContext: schema.ImportStatePassthroughContext, 28 | }, 29 | 30 | Schema: map[string]*schema.Schema{ 31 | "check_url": { 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Description: "URL of the heartbeat check", 35 | }, 36 | "contact_groups": { 37 | Type: schema.TypeSet, 38 | Optional: true, 39 | Description: "List of contact group IDs", 40 | Elem: &schema.Schema{ 41 | Type: schema.TypeString, 42 | ValidateFunc: intvalidation.StringIsNumerical, 43 | }, 44 | }, 45 | "monitored_resource": { 46 | Type: schema.TypeList, 47 | Optional: true, 48 | MaxItems: 1, 49 | Description: "Monitored resource configuration block. This describes the server under test", 50 | Elem: &schema.Resource{ 51 | Schema: map[string]*schema.Schema{ 52 | "host": { 53 | Type: schema.TypeString, 54 | Optional: true, 55 | Description: "Name of the hosting provider", 56 | ValidateFunc: validation.StringIsNotEmpty, 57 | }, 58 | }, 59 | }, 60 | }, 61 | "name": { 62 | Type: schema.TypeString, 63 | Required: true, 64 | Description: "Name of the check", 65 | ValidateFunc: validation.StringIsNotEmpty, 66 | }, 67 | "paused": { 68 | Type: schema.TypeBool, 69 | Optional: true, 70 | Default: false, 71 | Description: "Whether the check should be run", 72 | }, 73 | "period": { 74 | Type: schema.TypeInt, 75 | Required: true, 76 | Description: "Number of seconds since the last ping before the check is considered down.", 77 | ValidateFunc: validation.IntBetween(30, 172800), 78 | }, 79 | "tags": { 80 | Type: schema.TypeSet, 81 | Optional: true, 82 | Description: "List of tags", 83 | Elem: &schema.Schema{ 84 | Type: schema.TypeString, 85 | ValidateFunc: validation.StringIsNotEmpty, 86 | }, 87 | }, 88 | }, 89 | } 90 | } 91 | 92 | func resourceStatusCakeHeartbeatCheckCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 93 | client := meta.(*statuscake.Client) 94 | body := make(map[string]interface{}) 95 | 96 | contactGroups, err := expandHeartbeatCheckContactGroups(d.Get("contact_groups"), d) 97 | if err != nil { 98 | return diag.FromErr(err) 99 | } else if d.HasChange("contact_groups") { 100 | body["contact_groups"] = contactGroups 101 | } 102 | 103 | monitoredResource, err := expandHeartbeatCheckMonitoredResource(d.Get("monitored_resource"), d) 104 | if err != nil { 105 | return diag.FromErr(err) 106 | } else if d.HasChange("monitored_resource") { 107 | body = merge(body, monitoredResource.(map[string]interface{})) 108 | } 109 | 110 | name, err := expandHeartbeatCheckName(d.Get("name"), d) 111 | if err != nil { 112 | return diag.FromErr(err) 113 | } else if d.HasChange("name") { 114 | body["name"] = name 115 | } 116 | 117 | paused, err := expandHeartbeatCheckPaused(d.Get("paused"), d) 118 | if err != nil { 119 | return diag.FromErr(err) 120 | } else if d.HasChange("paused") { 121 | body["paused"] = paused 122 | } 123 | 124 | period, err := expandHeartbeatCheckPeriod(d.Get("period"), d) 125 | if err != nil { 126 | return diag.FromErr(err) 127 | } else if d.HasChange("period") { 128 | body["period"] = period 129 | } 130 | 131 | tags, err := expandHeartbeatCheckTags(d.Get("tags"), d) 132 | if err != nil { 133 | return diag.FromErr(err) 134 | } else if d.HasChange("tags") { 135 | body["tags"] = tags 136 | } 137 | 138 | log.Printf("[DEBUG] Request body: %+v", body) 139 | 140 | res, err := client.CreateHeartbeatTestWithData(ctx, body).Execute() 141 | if err != nil { 142 | return intdiag.FromErr("failed to create heartbeat check", err) 143 | } 144 | 145 | d.SetId(res.Data.NewID) 146 | return resourceStatusCakeHeartbeatCheckRead(ctx, d, meta) 147 | } 148 | 149 | func resourceStatusCakeHeartbeatCheckRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 150 | client := meta.(*statuscake.Client) 151 | id := d.Id() 152 | 153 | res, err := client.GetHeartbeatTest(ctx, id).Execute() 154 | 155 | // If the resource is not found then remove it from the state. 156 | if err, ok := err.(statuscake.APIError); ok && err.Status == http.StatusNotFound { 157 | d.SetId("") 158 | return nil 159 | } 160 | if err != nil { 161 | return diag.Errorf("failed to get heartbeat check with ID: %s, error: %s", id, err) 162 | } 163 | 164 | if err := d.Set("contact_groups", flattenHeartbeatCheckContactGroups(res.Data.ContactGroups, d)); err != nil { 165 | return diag.Errorf("failed to read contact groups: %s", err) 166 | } 167 | 168 | if err := d.Set("monitored_resource", flattenHeartbeatCheckMonitoredResource(res.Data, d)); err != nil { 169 | return diag.Errorf("failed to read monitored resource: %s", err) 170 | } 171 | 172 | if err := d.Set("name", flattenHeartbeatCheckName(res.Data.Name, d)); err != nil { 173 | return diag.Errorf("failed to read name: %s", err) 174 | } 175 | 176 | if err := d.Set("paused", flattenHeartbeatCheckPaused(res.Data.Paused, d)); err != nil { 177 | return diag.Errorf("failed to read paused: %s", err) 178 | } 179 | 180 | if err := d.Set("period", flattenHeartbeatCheckPeriod(res.Data.Period, d)); err != nil { 181 | return diag.Errorf("failed to read period: %s", err) 182 | } 183 | 184 | if err := d.Set("check_url", flattenHeartbeatCheckURL(res.Data.WebsiteURL, d)); err != nil { 185 | return diag.Errorf("failed to read check URL: %s", err) 186 | } 187 | 188 | if err := d.Set("tags", flattenHeartbeatCheckTags(res.Data.Tags, d)); err != nil { 189 | return diag.Errorf("failed to read tags: %s", err) 190 | } 191 | 192 | return nil 193 | } 194 | 195 | func resourceStatusCakeHeartbeatCheckUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 196 | client := meta.(*statuscake.Client) 197 | body := make(map[string]interface{}) 198 | id := d.Id() 199 | 200 | contactGroups, err := expandHeartbeatCheckContactGroups(d.Get("contact_groups"), d) 201 | if err != nil { 202 | return diag.FromErr(err) 203 | } else if d.HasChange("contact_groups") { 204 | body["contact_groups"] = contactGroups 205 | } 206 | 207 | monitoredResource, err := expandHeartbeatCheckMonitoredResource(d.Get("monitored_resource"), d) 208 | if err != nil { 209 | return diag.FromErr(err) 210 | } else if d.HasChange("monitored_resource") { 211 | body = merge(body, monitoredResource.(map[string]interface{})) 212 | } 213 | 214 | name, err := expandHeartbeatCheckName(d.Get("name"), d) 215 | if err != nil { 216 | return diag.FromErr(err) 217 | } else if d.HasChange("name") { 218 | body["name"] = name 219 | } 220 | 221 | paused, err := expandHeartbeatCheckPaused(d.Get("paused"), d) 222 | if err != nil { 223 | return diag.FromErr(err) 224 | } else if d.HasChange("paused") { 225 | body["paused"] = paused 226 | } 227 | 228 | period, err := expandHeartbeatCheckPeriod(d.Get("period"), d) 229 | if err != nil { 230 | return diag.FromErr(err) 231 | } else if d.HasChange("period") { 232 | body["period"] = period 233 | } 234 | 235 | tags, err := expandHeartbeatCheckTags(d.Get("tags"), d) 236 | if err != nil { 237 | return diag.FromErr(err) 238 | } else if d.HasChange("tags") { 239 | body["tags"] = tags 240 | } 241 | 242 | log.Printf("[DEBUG] Updating StatusCake heartbeat check with ID: %s", id) 243 | log.Printf("[DEBUG] Request body: %+v", body) 244 | 245 | if err := client.UpdateHeartbeatTestWithData(ctx, id, body).Execute(); err != nil { 246 | return intdiag.FromErr(fmt.Sprintf("failed to update heartbeat check with id %s", id), err) 247 | } 248 | 249 | return resourceStatusCakeHeartbeatCheckRead(ctx, d, meta) 250 | } 251 | 252 | func resourceStatusCakeHeartbeatCheckDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 253 | client := meta.(*statuscake.Client) 254 | id := d.Id() 255 | 256 | log.Printf("[DEBUG] Deleting StatusCake heartbeat check with ID: %s", id) 257 | 258 | if err := client.DeleteHeartbeatTest(ctx, id).Execute(); err != nil { 259 | return intdiag.FromErr(fmt.Sprintf("failed to delete heartbeat check with id %s", id), err) 260 | } 261 | 262 | return nil 263 | } 264 | 265 | func expandHeartbeatCheckContactGroups(v interface{}, d *schema.ResourceData) (interface{}, error) { 266 | return convertStringSet(v.(*schema.Set)), nil 267 | } 268 | 269 | func flattenHeartbeatCheckContactGroups(v interface{}, d *schema.ResourceData) interface{} { 270 | return v 271 | } 272 | 273 | func expandHeartbeatCheckHost(v interface{}, d *schema.ResourceData) (interface{}, error) { 274 | return v.(string), nil 275 | } 276 | 277 | func flattenHeartbeatCheckHost(v interface{}, d *schema.ResourceData) interface{} { 278 | return v 279 | } 280 | 281 | func expandHeartbeatCheckMonitoredResource(v interface{}, d *schema.ResourceData) (interface{}, error) { 282 | l := v.([]interface{}) 283 | 284 | if len(l) == 0 || l[0] == nil { 285 | // If the monitored resource is not set then return an empty map. This is 286 | // necessary for the Heartbeat API only because the monitored_resource block 287 | // is optional. Therefore when the entire block is removed then the "host" 288 | // field is not set. 289 | // 290 | // At present this causes the API to return an error. This is a bug in the 291 | // API and does have a fix ready to go. 292 | return map[string]interface{}{ 293 | "host": "", 294 | }, nil 295 | } 296 | 297 | original := l[0].(map[string]interface{}) 298 | transformed := make(map[string]interface{}) 299 | 300 | host, err := expandHeartbeatCheckHost(original["host"], d) 301 | if err != nil { 302 | return nil, err 303 | } else if d.HasChange("monitored_resource.0.host") { 304 | transformed["host"] = host 305 | } 306 | 307 | return transformed, nil 308 | } 309 | 310 | func flattenHeartbeatCheckMonitoredResource(v interface{}, d *schema.ResourceData) interface{} { 311 | data := v.(statuscake.HeartbeatTest) 312 | 313 | host := flattenHeartbeatCheckHost(data.Host, d) 314 | if !isValid(host) { 315 | return nil 316 | } 317 | 318 | return []map[string]interface{}{ 319 | { 320 | "host": host, 321 | }, 322 | } 323 | } 324 | 325 | func expandHeartbeatCheckName(v interface{}, d *schema.ResourceData) (interface{}, error) { 326 | return v.(string), nil 327 | } 328 | 329 | func flattenHeartbeatCheckName(v interface{}, d *schema.ResourceData) interface{} { 330 | return v 331 | } 332 | 333 | func expandHeartbeatCheckPaused(v interface{}, d *schema.ResourceData) (interface{}, error) { 334 | return v.(bool), nil 335 | } 336 | 337 | func flattenHeartbeatCheckPaused(v interface{}, d *schema.ResourceData) interface{} { 338 | return v 339 | } 340 | 341 | func expandHeartbeatCheckPeriod(v interface{}, d *schema.ResourceData) (interface{}, error) { 342 | return int32(v.(int)), nil 343 | } 344 | 345 | func flattenHeartbeatCheckPeriod(v interface{}, d *schema.ResourceData) interface{} { 346 | return v 347 | } 348 | 349 | func flattenHeartbeatCheckURL(v interface{}, d *schema.ResourceData) interface{} { 350 | return v 351 | } 352 | 353 | func expandHeartbeatCheckTags(v interface{}, d *schema.ResourceData) (interface{}, error) { 354 | return convertStringSet(v.(*schema.Set)), nil 355 | } 356 | 357 | func flattenHeartbeatCheckTags(v interface{}, d *schema.ResourceData) interface{} { 358 | return v 359 | } 360 | -------------------------------------------------------------------------------- /internal/provider/resource_pagespeed_check.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/StatusCakeDev/statuscake-go" 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 | intdiag "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/diag" 15 | intvalidation "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 16 | ) 17 | 18 | func resourceStatusCakePagespeedCheck() *schema.Resource { 19 | return &schema.Resource{ 20 | CreateContext: resourceStatusCakePagespeedCheckCreate, 21 | ReadContext: resourceStatusCakePagespeedCheckRead, 22 | UpdateContext: resourceStatusCakePagespeedCheckUpdate, 23 | DeleteContext: resourceStatusCakePagespeedCheckDelete, 24 | 25 | // Used by `terraform import`. 26 | Importer: &schema.ResourceImporter{ 27 | StateContext: schema.ImportStatePassthroughContext, 28 | }, 29 | 30 | Schema: map[string]*schema.Schema{ 31 | "alert_config": { 32 | Type: schema.TypeList, 33 | Required: true, 34 | MaxItems: 1, 35 | Description: "Alert configuration block. An empty block disables all alerts", 36 | Elem: &schema.Resource{ 37 | Schema: map[string]*schema.Schema{ 38 | "alert_bigger": { 39 | Type: schema.TypeInt, 40 | Optional: true, 41 | Default: 0, 42 | Description: "An alert will be sent if the size of the page is larger than this value (kb).", 43 | ValidateFunc: validation.IntAtLeast(0), 44 | }, 45 | "alert_slower": { 46 | Type: schema.TypeInt, 47 | Optional: true, 48 | Default: 0, 49 | Description: "An alert will be sent if the load time of the page exceeds this value (ms).", 50 | ValidateFunc: validation.IntAtLeast(0), 51 | }, 52 | "alert_smaller": { 53 | Type: schema.TypeInt, 54 | Optional: true, 55 | Default: 0, 56 | Description: "An alert will be sent if the size of the page is smaller than this value (kb).", 57 | ValidateFunc: validation.IntAtLeast(0), 58 | }, 59 | }, 60 | }, 61 | }, 62 | "check_interval": { 63 | Type: schema.TypeInt, 64 | Required: true, 65 | Description: "Number of seconds between checks", 66 | ValidateFunc: intvalidation.Int32InSlice(statuscake.PagespeedTestCheckRateValues()), 67 | }, 68 | "contact_groups": { 69 | Type: schema.TypeSet, 70 | Optional: true, 71 | Description: "List of contact group IDs", 72 | Elem: &schema.Schema{ 73 | Type: schema.TypeString, 74 | ValidateFunc: intvalidation.StringIsNumerical, 75 | }, 76 | }, 77 | "location": { 78 | Type: schema.TypeString, 79 | Computed: true, 80 | Description: "Assigned monitoring location on which checks will be run", 81 | }, 82 | "monitored_resource": { 83 | Type: schema.TypeList, 84 | Required: true, 85 | MaxItems: 1, 86 | Description: "Monitored resource configuration block. This describes the server under test", 87 | Elem: &schema.Resource{ 88 | Schema: map[string]*schema.Schema{ 89 | "address": { 90 | Type: schema.TypeString, 91 | Required: true, 92 | Description: "URL or IP address of the website under test", 93 | ValidateFunc: validation.Any(validation.IsURLWithHTTPorHTTPS, validation.IsIPAddress), 94 | }, 95 | }, 96 | }, 97 | }, 98 | "name": { 99 | Type: schema.TypeString, 100 | Required: true, 101 | Description: "Name of the check", 102 | ValidateFunc: validation.StringIsNotEmpty, 103 | }, 104 | "paused": { 105 | Type: schema.TypeBool, 106 | Optional: true, 107 | Default: false, 108 | Description: "Whether the check should be run", 109 | }, 110 | "region": { 111 | Type: schema.TypeString, 112 | Required: true, 113 | Description: "Region on which to run checks", 114 | ValidateFunc: validation.StringInSlice(statuscake.PagespeedTestRegionValues(), false), 115 | }, 116 | }, 117 | } 118 | } 119 | 120 | func resourceStatusCakePagespeedCheckCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 121 | client := meta.(*statuscake.Client) 122 | body := make(map[string]interface{}) 123 | 124 | config, err := expandPagespeedCheckAlertConfig(d.Get("alert_config"), d) 125 | if err != nil { 126 | return diag.FromErr(err) 127 | } else if d.HasChange("alert_config") { 128 | body = merge(body, config.(map[string]interface{})) 129 | } 130 | 131 | checkInterval, err := expandPagespeedCheckInterval(d.Get("check_interval"), d) 132 | if err != nil { 133 | return diag.FromErr(err) 134 | } else if d.HasChange("check_interval") { 135 | body["check_rate"] = checkInterval 136 | } 137 | 138 | contactGroups, err := expandPagespeedCheckContactGroups(d.Get("contact_groups"), d) 139 | if err != nil { 140 | return diag.FromErr(err) 141 | } else if d.HasChange("contact_groups") { 142 | body["contact_groups"] = contactGroups 143 | } 144 | 145 | name, err := expandPagespeedCheckName(d.Get("name"), d) 146 | if err != nil { 147 | return diag.FromErr(err) 148 | } else if d.HasChange("name") { 149 | body["name"] = name 150 | } 151 | 152 | monitoredResource, err := expandPagespeedCheckMonitoredResource(d.Get("monitored_resource"), d) 153 | if err != nil { 154 | return diag.FromErr(err) 155 | } else if d.HasChange("monitored_resource") { 156 | body = merge(body, monitoredResource.(map[string]interface{})) 157 | } 158 | 159 | paused, err := expandPagespeedCheckPaused(d.Get("paused"), d) 160 | if err != nil { 161 | return diag.FromErr(err) 162 | } else if d.HasChange("paused") { 163 | body["paused"] = paused 164 | } 165 | 166 | region, err := expandPagespeedCheckRegion(d.Get("region"), d) 167 | if err != nil { 168 | return diag.FromErr(err) 169 | } else if d.HasChange("region") { 170 | body["region"] = region 171 | } 172 | 173 | log.Print("[DEBUG] Creating StatusCake pagespeed check") 174 | log.Printf("[DEBUG] Request body: %+v", body) 175 | 176 | res, err := client.CreatePagespeedTestWithData(ctx, body).Execute() 177 | if err != nil { 178 | return intdiag.FromErr("failed to create pagespeed check", err) 179 | } 180 | 181 | d.SetId(res.Data.NewID) 182 | return resourceStatusCakePagespeedCheckRead(ctx, d, meta) 183 | } 184 | 185 | func resourceStatusCakePagespeedCheckRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 186 | client := meta.(*statuscake.Client) 187 | id := d.Id() 188 | 189 | res, err := client.GetPagespeedTest(ctx, id).Execute() 190 | 191 | // If the resource is not found then remove it from the state. 192 | if err, ok := err.(statuscake.APIError); ok && err.Status == http.StatusNotFound { 193 | d.SetId("") 194 | return nil 195 | } 196 | if err != nil { 197 | return diag.Errorf("failed to get pagespeed check with ID: %s, error: %s", id, err) 198 | } 199 | 200 | if err := d.Set("alert_config", flattenPagespeedCheckAlertConfig(res.Data, d)); err != nil { 201 | return diag.Errorf("failed to read alert config: %s", err) 202 | } 203 | 204 | if err := d.Set("check_interval", flattenPagespeedCheckInterval(res.Data.CheckRate, d)); err != nil { 205 | return diag.Errorf("failed to read check interval: %s", err) 206 | } 207 | 208 | if err := d.Set("contact_groups", flattenPagespeedCheckContactGroups(res.Data.ContactGroups, d)); err != nil { 209 | return diag.Errorf("failed to read contact groups: %s", err) 210 | } 211 | 212 | if err := d.Set("location", flattenPagespeedCheckLocation(res.Data.Location, d)); err != nil { 213 | return diag.Errorf("failed to read location: %s", err) 214 | } 215 | 216 | if err := d.Set("monitored_resource", flattenPagespeedCheckMonitoredResource(res.Data, d)); err != nil { 217 | return diag.Errorf("failed to read monitored resource: %s", err) 218 | } 219 | 220 | if err := d.Set("name", flattenPagespeedCheckName(res.Data.Name, d)); err != nil { 221 | return diag.Errorf("failed to read name: %s", err) 222 | } 223 | 224 | if err := d.Set("paused", flattenPagespeedCheckPaused(res.Data.Paused, d)); err != nil { 225 | return diag.Errorf("failed to read paused: %s", err) 226 | } 227 | 228 | return nil 229 | } 230 | 231 | func resourceStatusCakePagespeedCheckUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 232 | client := meta.(*statuscake.Client) 233 | body := make(map[string]interface{}) 234 | id := d.Id() 235 | 236 | config, err := expandPagespeedCheckAlertConfig(d.Get("alert_config"), d) 237 | if err != nil { 238 | return diag.FromErr(err) 239 | } else if d.HasChange("alert_config") { 240 | body = merge(body, config.(map[string]interface{})) 241 | } 242 | 243 | checkInterval, err := expandPagespeedCheckInterval(d.Get("check_interval"), d) 244 | if err != nil { 245 | return diag.FromErr(err) 246 | } else if d.HasChange("check_interval") { 247 | body["check_rate"] = checkInterval 248 | } 249 | 250 | contactGroups, err := expandPagespeedCheckContactGroups(d.Get("contact_groups"), d) 251 | if err != nil { 252 | return diag.FromErr(err) 253 | } else if d.HasChange("contact_groups") { 254 | body["contact_groups"] = contactGroups 255 | } 256 | 257 | name, err := expandPagespeedCheckName(d.Get("name"), d) 258 | if err != nil { 259 | return diag.FromErr(err) 260 | } else if d.HasChange("name") { 261 | body["name"] = name 262 | } 263 | 264 | monitoredResource, err := expandPagespeedCheckMonitoredResource(d.Get("monitored_resource"), d) 265 | if err != nil { 266 | return diag.FromErr(err) 267 | } else if d.HasChange("monitored_resource") { 268 | body = merge(body, monitoredResource.(map[string]interface{})) 269 | } 270 | 271 | paused, err := expandPagespeedCheckPaused(d.Get("paused"), d) 272 | if err != nil { 273 | return diag.FromErr(err) 274 | } else if d.HasChange("paused") { 275 | body["paused"] = paused 276 | } 277 | 278 | region, err := expandPagespeedCheckRegion(d.Get("region"), d) 279 | if err != nil { 280 | return diag.FromErr(err) 281 | } else if d.HasChange("region") { 282 | body["region"] = region 283 | } 284 | 285 | log.Printf("[DEBUG] Updating StatusCake pagespeed check with ID: %s", id) 286 | log.Printf("[DEBUG] Request body: %+v", body) 287 | 288 | if err := client.UpdatePagespeedTestWithData(ctx, id, body).Execute(); err != nil { 289 | return intdiag.FromErr(fmt.Sprintf("failed to update pagespeed check with id %s", id), err) 290 | } 291 | 292 | return resourceStatusCakePagespeedCheckRead(ctx, d, meta) 293 | } 294 | 295 | func resourceStatusCakePagespeedCheckDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 296 | client := meta.(*statuscake.Client) 297 | id := d.Id() 298 | 299 | log.Printf("[DEBUG] Deleting StatusCake pagespeed check with ID: %s", id) 300 | 301 | if err := client.DeletePagespeedTest(ctx, id).Execute(); err != nil { 302 | return intdiag.FromErr(fmt.Sprintf("failed to delete pagespeed check with id %s", id), err) 303 | } 304 | 305 | return nil 306 | } 307 | 308 | func expandPagespeedCheckAddress(v interface{}, d *schema.ResourceData) (interface{}, error) { 309 | return v.(string), nil 310 | } 311 | 312 | func flattenPagespeedCheckAddress(v interface{}, d *schema.ResourceData) interface{} { 313 | return v 314 | } 315 | 316 | func expandPagespeedCheckAlertBigger(v interface{}, d *schema.ResourceData) (interface{}, error) { 317 | return int32(v.(int)), nil 318 | } 319 | 320 | func flattenPagespeedCheckAlertBigger(v interface{}, d *schema.ResourceData) interface{} { 321 | return v 322 | } 323 | 324 | func expandPagespeedCheckAlertConfig(v interface{}, d *schema.ResourceData) (interface{}, error) { 325 | l := v.([]interface{}) 326 | 327 | if len(l) == 0 || l[0] == nil { 328 | return map[string]interface{}{}, nil 329 | } 330 | 331 | original := l[0].(map[string]interface{}) 332 | transformed := make(map[string]interface{}) 333 | 334 | bigger, err := expandPagespeedCheckAlertBigger(original["alert_bigger"], d) 335 | if err != nil { 336 | return nil, err 337 | } else if d.HasChange("alert_config.0.alert_bigger") { 338 | transformed["alert_bigger"] = bigger 339 | } 340 | 341 | slower, err := expandPagespeedCheckAlertSlower(original["alert_slower"], d) 342 | if err != nil { 343 | return nil, err 344 | } else if d.HasChange("alert_config.0.alert_slower") { 345 | transformed["alert_slower"] = slower 346 | } 347 | 348 | smaller, err := expandPagespeedCheckAlertSmaller(original["alert_smaller"], d) 349 | if err != nil { 350 | return nil, err 351 | } else if d.HasChange("alert_config.0.alert_smaller") { 352 | transformed["alert_smaller"] = smaller 353 | } 354 | 355 | return transformed, nil 356 | } 357 | 358 | func flattenPagespeedCheckAlertConfig(v interface{}, d *schema.ResourceData) interface{} { 359 | original := v.(statuscake.PagespeedTest) 360 | return []map[string]interface{}{ 361 | { 362 | "alert_bigger": flattenPagespeedCheckAlertBigger(original.AlertBigger, d), 363 | "alert_slower": flattenPagespeedCheckAlertSlower(original.AlertSlower, d), 364 | "alert_smaller": flattenPagespeedCheckAlertSmaller(original.AlertSmaller, d), 365 | }, 366 | } 367 | } 368 | 369 | func expandPagespeedCheckAlertSlower(v interface{}, d *schema.ResourceData) (interface{}, error) { 370 | return int64(v.(int)), nil 371 | } 372 | 373 | func flattenPagespeedCheckAlertSlower(v interface{}, d *schema.ResourceData) interface{} { 374 | return v 375 | } 376 | 377 | func expandPagespeedCheckAlertSmaller(v interface{}, d *schema.ResourceData) (interface{}, error) { 378 | return int32(v.(int)), nil 379 | } 380 | 381 | func flattenPagespeedCheckAlertSmaller(v interface{}, d *schema.ResourceData) interface{} { 382 | return v 383 | } 384 | 385 | func expandPagespeedCheckInterval(v interface{}, d *schema.ResourceData) (interface{}, error) { 386 | return statuscake.PagespeedTestCheckRate(v.(int)), nil 387 | } 388 | 389 | func flattenPagespeedCheckInterval(v interface{}, d *schema.ResourceData) interface{} { 390 | return int(v.(statuscake.PagespeedTestCheckRate)) 391 | } 392 | 393 | func expandPagespeedCheckContactGroups(v interface{}, d *schema.ResourceData) (interface{}, error) { 394 | return convertStringSet(v.(*schema.Set)), nil 395 | } 396 | 397 | func flattenPagespeedCheckContactGroups(v interface{}, d *schema.ResourceData) interface{} { 398 | return v 399 | } 400 | 401 | func flattenPagespeedCheckLocation(v interface{}, d *schema.ResourceData) interface{} { 402 | return v 403 | } 404 | 405 | func expandPagespeedCheckMonitoredResource(v interface{}, d *schema.ResourceData) (interface{}, error) { 406 | l := v.([]interface{}) 407 | 408 | if len(l) == 0 || l[0] == nil { 409 | return map[string]interface{}{}, nil 410 | } 411 | 412 | original := l[0].(map[string]interface{}) 413 | transformed := make(map[string]interface{}) 414 | 415 | address, err := expandPagespeedCheckAddress(original["address"], d) 416 | if err != nil { 417 | return nil, err 418 | } else if d.HasChange("monitored_resource.0.address") { 419 | transformed["website_url"] = address 420 | } 421 | 422 | return transformed, nil 423 | } 424 | 425 | func flattenPagespeedCheckMonitoredResource(v interface{}, d *schema.ResourceData) interface{} { 426 | data := v.(statuscake.PagespeedTest) 427 | return []map[string]interface{}{ 428 | { 429 | "address": flattenPagespeedCheckAddress(data.WebsiteURL, d), 430 | }, 431 | } 432 | } 433 | 434 | func expandPagespeedCheckName(v interface{}, d *schema.ResourceData) (interface{}, error) { 435 | return v.(string), nil 436 | } 437 | 438 | func flattenPagespeedCheckName(v interface{}, d *schema.ResourceData) interface{} { 439 | return v 440 | } 441 | 442 | func expandPagespeedCheckPaused(v interface{}, d *schema.ResourceData) (interface{}, error) { 443 | return v.(bool), nil 444 | } 445 | 446 | func flattenPagespeedCheckPaused(v interface{}, d *schema.ResourceData) interface{} { 447 | return v 448 | } 449 | 450 | func expandPagespeedCheckRegion(v interface{}, d *schema.ResourceData) (interface{}, error) { 451 | return statuscake.PagespeedTestRegion(v.(string)), nil 452 | } 453 | -------------------------------------------------------------------------------- /internal/provider/resource_ssl_check.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/StatusCakeDev/statuscake-go" 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 | intdiag "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/diag" 15 | intvalidation "github.com/StatusCakeDev/terraform-provider-statuscake/v2/internal/provider/validation" 16 | ) 17 | 18 | func resourceStatusCakeSSLCheck() *schema.Resource { 19 | return &schema.Resource{ 20 | CreateContext: resourceStatusCakeSSLCheckCreate, 21 | ReadContext: resourceStatusCakeSSLCheckRead, 22 | UpdateContext: resourceStatusCakeSSLCheckUpdate, 23 | DeleteContext: resourceStatusCakeSSLCheckDelete, 24 | 25 | // Used by `terraform import`. 26 | Importer: &schema.ResourceImporter{ 27 | StateContext: schema.ImportStatePassthroughContext, 28 | }, 29 | 30 | Schema: map[string]*schema.Schema{ 31 | "alert_config": { 32 | Type: schema.TypeList, 33 | Required: true, 34 | MaxItems: 1, 35 | Description: "Alert configuration block", 36 | Elem: &schema.Resource{ 37 | Schema: map[string]*schema.Schema{ 38 | "alert_at": { 39 | Type: schema.TypeSet, 40 | Required: true, 41 | MinItems: 3, 42 | MaxItems: 3, 43 | Description: "List representing when alerts should be sent (days). Must be exactly 3 numerical values", 44 | Elem: &schema.Schema{ 45 | Type: schema.TypeInt, 46 | ValidateFunc: validation.IntAtLeast(1), 47 | }, 48 | }, 49 | "on_broken": { 50 | Type: schema.TypeBool, 51 | Optional: true, 52 | Default: false, 53 | Description: "Whether to enable alerts when SSL certificate issues are found", 54 | }, 55 | "on_expiry": { 56 | Type: schema.TypeBool, 57 | Optional: true, 58 | Default: false, 59 | Description: "Whether to enable alerts when the SSL certificate is to expire", 60 | }, 61 | "on_mixed": { 62 | Type: schema.TypeBool, 63 | Optional: true, 64 | Default: false, 65 | Description: "Whether to enable alerts when mixed content is found", 66 | }, 67 | "on_reminder": { 68 | Type: schema.TypeBool, 69 | Optional: true, 70 | Default: false, 71 | Description: "Whether to enable alert reminders", 72 | }, 73 | }, 74 | }, 75 | }, 76 | "check_interval": { 77 | Type: schema.TypeInt, 78 | Required: true, 79 | Description: "Number of seconds between checks", 80 | ValidateFunc: intvalidation.Int32InSlice(statuscake.SSLTestCheckRateValues()), 81 | }, 82 | "contact_groups": { 83 | Type: schema.TypeSet, 84 | Optional: true, 85 | Description: "List of contact group IDs", 86 | Elem: &schema.Schema{ 87 | Type: schema.TypeString, 88 | ValidateFunc: intvalidation.StringIsNumerical, 89 | }, 90 | }, 91 | "follow_redirects": { 92 | Type: schema.TypeBool, 93 | Optional: true, 94 | Default: false, 95 | Description: "Whether to follow redirects when testing. Disabled by default", 96 | }, 97 | "monitored_resource": { 98 | Type: schema.TypeList, 99 | Required: true, 100 | MaxItems: 1, 101 | Description: "Monitored resource configuration block. This describes the server under test", 102 | Elem: &schema.Resource{ 103 | Schema: map[string]*schema.Schema{ 104 | "address": { 105 | Type: schema.TypeString, 106 | Required: true, 107 | Description: "URL of the server under test", 108 | ValidateFunc: validation.IsURLWithHTTPorHTTPS, 109 | }, 110 | "hostname": { 111 | Type: schema.TypeString, 112 | Optional: true, 113 | Description: "Hostname of the server under test", 114 | ValidateFunc: validation.StringIsNotEmpty, 115 | }, 116 | }, 117 | }, 118 | }, 119 | "paused": { 120 | Type: schema.TypeBool, 121 | Optional: true, 122 | Default: false, 123 | Description: "Whether the check should be run", 124 | }, 125 | "user_agent": { 126 | Type: schema.TypeString, 127 | Optional: true, 128 | Description: "Custom user agent string set when testing", 129 | ValidateFunc: validation.StringIsNotEmpty, 130 | }, 131 | }, 132 | } 133 | } 134 | 135 | func resourceStatusCakeSSLCheckCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 136 | client := meta.(*statuscake.Client) 137 | body := make(map[string]interface{}) 138 | 139 | config, err := expandSSLCheckAlertConfig(d.Get("alert_config"), d) 140 | if err != nil { 141 | return diag.FromErr(err) 142 | } else if d.HasChange("alert_config") { 143 | body = merge(body, config.(map[string]interface{})) 144 | } 145 | 146 | checkInterval, err := expandSSLCheckInterval(d.Get("check_interval"), d) 147 | if err != nil { 148 | return diag.FromErr(err) 149 | } else if d.HasChange("check_interval") { 150 | body["check_rate"] = checkInterval 151 | } 152 | 153 | contactGroups, err := expandSSLCheckContactGroups(d.Get("contact_groups"), d) 154 | if err != nil { 155 | return diag.FromErr(err) 156 | } else if d.HasChange("contact_groups") { 157 | body["contact_groups"] = contactGroups 158 | } 159 | 160 | followRedirects, err := expandSSLCheckFollowRedirects(d.Get("follow_redirects"), d) 161 | if err != nil { 162 | return diag.FromErr(err) 163 | } else if d.HasChange("follow_redirects") { 164 | body["follow_redirects"] = followRedirects 165 | } 166 | 167 | monitoredResource, err := expandSSLCheckMonitoredResource(d.Get("monitored_resource"), d) 168 | if err != nil { 169 | return diag.FromErr(err) 170 | } else if d.HasChange("monitored_resource") { 171 | body = merge(body, monitoredResource.(map[string]interface{})) 172 | } 173 | 174 | paused, err := expandSSLCheckPaused(d.Get("paused"), d) 175 | if err != nil { 176 | return diag.FromErr(err) 177 | } else if d.HasChange("paused") { 178 | body["paused"] = paused 179 | } 180 | 181 | userAgent, err := expandSSLCheckUserAgent(d.Get("user_agent"), d) 182 | if err != nil { 183 | return diag.FromErr(err) 184 | } else if d.HasChange("user_agent") { 185 | body["user_agent"] = userAgent 186 | } 187 | 188 | log.Print("[DEBUG] Creating StatusCake SSL check") 189 | log.Printf("[DEBUG] Request body: %+v", body) 190 | 191 | res, err := client.CreateSslTestWithData(ctx, body).Execute() 192 | if err != nil { 193 | return intdiag.FromErr("failed to create SSL check", err) 194 | } 195 | 196 | d.SetId(res.Data.NewID) 197 | return resourceStatusCakeSSLCheckRead(ctx, d, meta) 198 | } 199 | 200 | func resourceStatusCakeSSLCheckRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 201 | client := meta.(*statuscake.Client) 202 | id := d.Id() 203 | 204 | res, err := client.GetSslTest(ctx, id).Execute() 205 | 206 | // If the resource is not found then remove it from the state. 207 | if err, ok := err.(statuscake.APIError); ok && err.Status == http.StatusNotFound { 208 | d.SetId("") 209 | return nil 210 | } 211 | if err != nil { 212 | return diag.Errorf("failed to get SSL check with ID: %s, error: %s", id, err) 213 | } 214 | 215 | if err := d.Set("alert_config", flattenSSLCheckAlertConfig(res.Data, d)); err != nil { 216 | return diag.Errorf("failed to read alert config: %s", err) 217 | } 218 | 219 | if err := d.Set("check_interval", flattenSSLCheckInterval(res.Data.CheckRate, d)); err != nil { 220 | return diag.Errorf("failed to read check interval: %s", err) 221 | } 222 | 223 | if err := d.Set("contact_groups", flattenSSLCheckContactGroups(res.Data.ContactGroups, d)); err != nil { 224 | return diag.Errorf("failed to read contact groups: %s", err) 225 | } 226 | 227 | if err := d.Set("follow_redirects", flattenSSLCheckFollowRedirects(res.Data.FollowRedirects, d)); err != nil { 228 | return diag.Errorf("failed to read follow redirects: %s", err) 229 | } 230 | 231 | if err := d.Set("monitored_resource", flattenSSLCheckMonitoredResource(res.Data, d)); err != nil { 232 | return diag.Errorf("failed to read monitored resource: %s", err) 233 | } 234 | 235 | if err := d.Set("paused", flattenSSLCheckPaused(res.Data.Paused, d)); err != nil { 236 | return diag.Errorf("failed to read paused: %s", err) 237 | } 238 | 239 | if err := d.Set("user_agent", flattenSSLCheckUserAgent(res.Data.UserAgent, d)); err != nil { 240 | return diag.Errorf("failed to read user agent: %s", err) 241 | } 242 | 243 | return nil 244 | } 245 | 246 | func resourceStatusCakeSSLCheckUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 247 | client := meta.(*statuscake.Client) 248 | body := make(map[string]interface{}) 249 | id := d.Id() 250 | 251 | config, err := expandSSLCheckAlertConfig(d.Get("alert_config"), d) 252 | if err != nil { 253 | return diag.FromErr(err) 254 | } else if d.HasChange("alert_config") { 255 | body = merge(body, config.(map[string]interface{})) 256 | } 257 | 258 | checkInterval, err := expandSSLCheckInterval(d.Get("check_interval"), d) 259 | if err != nil { 260 | return diag.FromErr(err) 261 | } else if d.HasChange("check_interval") { 262 | body["check_rate"] = checkInterval 263 | } 264 | 265 | contactGroups, err := expandSSLCheckContactGroups(d.Get("contact_groups"), d) 266 | if err != nil { 267 | return diag.FromErr(err) 268 | } else if d.HasChange("contact_groups") { 269 | body["contact_groups"] = contactGroups 270 | } 271 | 272 | followRedirects, err := expandSSLCheckFollowRedirects(d.Get("follow_redirects"), d) 273 | if err != nil { 274 | return diag.FromErr(err) 275 | } else if d.HasChange("follow_redirects") { 276 | body["follow_redirects"] = followRedirects 277 | } 278 | 279 | monitoredResource, err := expandSSLCheckMonitoredResource(d.Get("monitored_resource"), d) 280 | if err != nil { 281 | return diag.FromErr(err) 282 | } else if d.HasChange("monitored_resource") { 283 | body = merge(body, monitoredResource.(map[string]interface{})) 284 | } 285 | 286 | paused, err := expandSSLCheckPaused(d.Get("paused"), d) 287 | if err != nil { 288 | return diag.FromErr(err) 289 | } else if d.HasChange("paused") { 290 | body["paused"] = paused 291 | } 292 | 293 | userAgent, err := expandSSLCheckUserAgent(d.Get("user_agent"), d) 294 | if err != nil { 295 | return diag.FromErr(err) 296 | } else if d.HasChange("user_agent") { 297 | body["user_agent"] = userAgent 298 | } 299 | 300 | log.Printf("[DEBUG] Updating StatusCake SSL check with ID: %s", id) 301 | log.Printf("[DEBUG] Request body: %+v", body) 302 | 303 | if err := client.UpdateSslTestWithData(ctx, id, body).Execute(); err != nil { 304 | return intdiag.FromErr(fmt.Sprintf("failed to update SSL check with id %s", id), err) 305 | } 306 | 307 | return resourceStatusCakeSSLCheckRead(ctx, d, meta) 308 | } 309 | 310 | func resourceStatusCakeSSLCheckDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { 311 | client := meta.(*statuscake.Client) 312 | id := d.Id() 313 | 314 | log.Printf("[DEBUG] Deleting StatusCake SSL check with ID: %s", id) 315 | 316 | if err := client.DeleteSslTest(ctx, id).Execute(); err != nil { 317 | return intdiag.FromErr(fmt.Sprintf("failed to delete SSL check with id %s", id), err) 318 | } 319 | 320 | return nil 321 | } 322 | 323 | func expandSSLCheckAddress(v interface{}, d *schema.ResourceData) (interface{}, error) { 324 | return v.(string), nil 325 | } 326 | 327 | func flattenSSLCheckAddress(v interface{}, d *schema.ResourceData) interface{} { 328 | return v 329 | } 330 | 331 | func expandSSLCheckAlertAt(v interface{}, d *schema.ResourceData) (interface{}, error) { 332 | return convertInt32Set(v.(*schema.Set)), nil 333 | } 334 | 335 | func flattenSSLCheckAlertAt(v interface{}, d *schema.ResourceData) interface{} { 336 | return v 337 | } 338 | 339 | func expandSSLCheckAlertConfig(v interface{}, d *schema.ResourceData) (interface{}, error) { 340 | l := v.([]interface{}) 341 | 342 | if len(l) == 0 || l[0] == nil { 343 | return map[string]interface{}{}, nil 344 | } 345 | 346 | original := l[0].(map[string]interface{}) 347 | transformed := make(map[string]interface{}) 348 | 349 | alertAt, err := expandSSLCheckAlertAt(original["alert_at"], d) 350 | if err != nil { 351 | return nil, err 352 | } else if d.HasChange("alert_config.0.alert_at") { 353 | transformed["alert_at"] = alertAt 354 | } 355 | 356 | broken, err := expandSSLCheckOnBroken(original["on_broken"], d) 357 | if err != nil { 358 | return nil, err 359 | } else if d.HasChange("alert_config.0.on_broken") { 360 | transformed["alert_broken"] = broken 361 | } 362 | 363 | expiry, err := expandSSLCheckOnExpiry(original["on_expiry"], d) 364 | if err != nil { 365 | return nil, err 366 | } else if d.HasChange("alert_config.0.on_expiry") { 367 | transformed["alert_expiry"] = expiry 368 | } 369 | 370 | mixed, err := expandSSLCheckOnMixed(original["on_mixed"], d) 371 | if err != nil { 372 | return nil, err 373 | } else if d.HasChange("alert_config.0.on_mixed") { 374 | transformed["alert_mixed"] = mixed 375 | } 376 | 377 | reminder, err := expandSSLCheckOnReminder(original["on_reminder"], d) 378 | if err != nil { 379 | return nil, err 380 | } else if d.HasChange("alert_config.0.on_reminder") { 381 | transformed["alert_reminder"] = reminder 382 | } 383 | 384 | return transformed, nil 385 | } 386 | 387 | func flattenSSLCheckAlertConfig(v interface{}, d *schema.ResourceData) interface{} { 388 | original := v.(statuscake.SSLTest) 389 | return []map[string]interface{}{ 390 | { 391 | "alert_at": flattenSSLCheckAlertAt(original.AlertAt, d), 392 | "on_broken": flattenSSLCheckOnBroken(original.AlertBroken, d), 393 | "on_expiry": flattenSSLCheckOnExpiry(original.AlertExpiry, d), 394 | "on_mixed": flattenSSLCheckOnMixed(original.AlertMixed, d), 395 | "on_reminder": flattenSSLCheckOnReminder(original.AlertReminder, d), 396 | }, 397 | } 398 | } 399 | 400 | func expandSSLCheckInterval(v interface{}, d *schema.ResourceData) (interface{}, error) { 401 | return statuscake.SSLTestCheckRate(v.(int)), nil 402 | } 403 | 404 | func flattenSSLCheckInterval(v interface{}, d *schema.ResourceData) interface{} { 405 | return int(v.(statuscake.SSLTestCheckRate)) 406 | } 407 | 408 | func expandSSLCheckContactGroups(v interface{}, d *schema.ResourceData) (interface{}, error) { 409 | return convertStringSet(v.(*schema.Set)), nil 410 | } 411 | 412 | func flattenSSLCheckContactGroups(v interface{}, d *schema.ResourceData) interface{} { 413 | return v 414 | } 415 | 416 | func expandSSLCheckFollowRedirects(v interface{}, d *schema.ResourceData) (interface{}, error) { 417 | return v.(bool), nil 418 | } 419 | 420 | func flattenSSLCheckFollowRedirects(v interface{}, d *schema.ResourceData) interface{} { 421 | return v 422 | } 423 | 424 | func expandSSLCheckHostname(v interface{}, d *schema.ResourceData) (interface{}, error) { 425 | return v.(string), nil 426 | } 427 | 428 | func flattenSSLCheckHostname(v interface{}, d *schema.ResourceData) interface{} { 429 | return v 430 | } 431 | 432 | func expandSSLCheckMonitoredResource(v interface{}, d *schema.ResourceData) (interface{}, error) { 433 | l := v.([]interface{}) 434 | 435 | if len(l) == 0 || l[0] == nil { 436 | return map[string]interface{}{}, nil 437 | } 438 | 439 | original := l[0].(map[string]interface{}) 440 | transformed := make(map[string]interface{}) 441 | 442 | address, err := expandSSLCheckAddress(original["address"], d) 443 | if err != nil { 444 | return nil, err 445 | } else if d.HasChange("monitored_resource.0.address") { 446 | transformed["website_url"] = address 447 | } 448 | 449 | hostname, err := expandSSLCheckHostname(original["hostname"], d) 450 | if err != nil { 451 | return nil, err 452 | } else if d.HasChange("monitored_resource.0.hostname") { 453 | transformed["hostname"] = hostname 454 | } 455 | 456 | return transformed, nil 457 | } 458 | 459 | func flattenSSLCheckMonitoredResource(v interface{}, d *schema.ResourceData) interface{} { 460 | data := v.(statuscake.SSLTest) 461 | return []map[string]interface{}{ 462 | { 463 | "address": flattenSSLCheckAddress(data.WebsiteURL, d), 464 | "hostname": flattenSSLCheckHostname(data.Hostname, d), 465 | }, 466 | } 467 | } 468 | 469 | func expandSSLCheckOnBroken(v interface{}, d *schema.ResourceData) (interface{}, error) { 470 | return v.(bool), nil 471 | } 472 | 473 | func flattenSSLCheckOnBroken(v interface{}, d *schema.ResourceData) interface{} { 474 | return v 475 | } 476 | 477 | func expandSSLCheckOnExpiry(v interface{}, d *schema.ResourceData) (interface{}, error) { 478 | return v.(bool), nil 479 | } 480 | 481 | func flattenSSLCheckOnExpiry(v interface{}, d *schema.ResourceData) interface{} { 482 | return v 483 | } 484 | 485 | func expandSSLCheckOnMixed(v interface{}, d *schema.ResourceData) (interface{}, error) { 486 | return v.(bool), nil 487 | } 488 | 489 | func flattenSSLCheckOnMixed(v interface{}, d *schema.ResourceData) interface{} { 490 | return v 491 | } 492 | 493 | func expandSSLCheckOnReminder(v interface{}, d *schema.ResourceData) (interface{}, error) { 494 | return v.(bool), nil 495 | } 496 | 497 | func flattenSSLCheckOnReminder(v interface{}, d *schema.ResourceData) interface{} { 498 | return v 499 | } 500 | 501 | func expandSSLCheckPaused(v interface{}, d *schema.ResourceData) (interface{}, error) { 502 | return v.(bool), nil 503 | } 504 | 505 | func flattenSSLCheckPaused(v interface{}, d *schema.ResourceData) interface{} { 506 | return v 507 | } 508 | 509 | func expandSSLCheckUserAgent(v interface{}, d *schema.ResourceData) (interface{}, error) { 510 | return v.(string), nil 511 | } 512 | 513 | func flattenSSLCheckUserAgent(v interface{}, d *schema.ResourceData) interface{} { 514 | return v 515 | } 516 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /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/StatusCakeDev/statuscake-go v1.3.0 h1:Ok6LIZUKhoe/ykcUuIzS8zg6MbtCrO3I1ZBVFl3TLMA= 18 | github.com/StatusCakeDev/statuscake-go v1.3.0/go.mod h1:YefpyhDUs1gLCKXG5F9pPcZAp3rk4JyNkb9dfHbgr1I= 19 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 20 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 21 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= 22 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 23 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 24 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 25 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 26 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= 27 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 28 | github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= 29 | github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 30 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 31 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 32 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 33 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 34 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 35 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 36 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 39 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 41 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 42 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 43 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 44 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 45 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 46 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 47 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 48 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 49 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 50 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 51 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= 52 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= 53 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 54 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 55 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 56 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= 106 | github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= 107 | github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= 108 | github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= 109 | github.com/hashicorp/terraform-plugin-docs v0.24.0 h1:YNZYd+8cpYclQyXbl1EEngbld8w7/LPOm99GD5nikIU= 110 | github.com/hashicorp/terraform-plugin-docs v0.24.0/go.mod h1:YLg+7LEwVmRuJc0EuCw0SPLxuQXw5mW8iJ5ml/kvi+o= 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 129 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 130 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= 131 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= 132 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 133 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 134 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 135 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 136 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 137 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 138 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 139 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 140 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 141 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 142 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 143 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 144 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 145 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 146 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 147 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 148 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 149 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 150 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 151 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 152 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 153 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 154 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 155 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 156 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 157 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 158 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 159 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 160 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 161 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 162 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 163 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 164 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 165 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 166 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 167 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 168 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 169 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 170 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= 171 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 172 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 173 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 174 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 175 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 176 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 177 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 178 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 179 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 180 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 181 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 182 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 183 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 184 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 185 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 186 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 187 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 188 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 189 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 190 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 191 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 192 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 193 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 194 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 195 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 196 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 197 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 198 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 199 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 200 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 201 | github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU= 202 | github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 203 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= 204 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= 205 | github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= 206 | github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= 207 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= 208 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= 209 | go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= 210 | go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= 211 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 212 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 213 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 214 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 215 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 216 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 217 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 218 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 219 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 220 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 221 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 222 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 223 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 224 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 225 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 226 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 227 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 228 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= 229 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 230 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 231 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 232 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 235 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 236 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 237 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 238 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 239 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 240 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 244 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 245 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 246 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 248 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 249 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 250 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 251 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 252 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 253 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 254 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 255 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 256 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 257 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 258 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 259 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 260 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 261 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 262 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 263 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 264 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 265 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 266 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 267 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 268 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 269 | golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 270 | golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 271 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 272 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 273 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 274 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 275 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 276 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 277 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 278 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 279 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 280 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 281 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 282 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 283 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= 284 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 285 | google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= 286 | google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 287 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 288 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 289 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 290 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 291 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 292 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 293 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 294 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 295 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 296 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 297 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 298 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 299 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 300 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 301 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 302 | --------------------------------------------------------------------------------