├── .github └── workflows │ ├── CODEOWNERS │ ├── go.yml │ └── release.yml ├── .licenserignore ├── .gitignore ├── examples ├── provider │ └── provider.tf ├── README.md └── resources │ ├── checkmate_local_command │ └── resource.tf │ ├── checkmate_tcp_echo │ └── resource.tf │ └── checkmate_http_health │ └── resource.tf ├── terraform-registry-manifest.json ├── docs ├── index.md └── resources │ ├── local_command.md │ ├── tcp_echo.md │ └── http_health.md ├── tools └── tools.go ├── pkg ├── provider.go ├── provider │ ├── provider_test.go │ ├── resource_local_command_test.go │ ├── provider.go │ ├── resource_tcp_echo_test.go │ ├── resource_http_health_test.go │ ├── resource_http_health.go │ ├── resource_tcp_echo.go │ └── resource_local_command.go ├── modifiers │ ├── default_int64.go │ └── default_string.go ├── helpers │ └── retry.go └── healthcheck │ ├── http_test.go │ └── http.go ├── Makefile ├── README.md ├── .goreleaser.yml ├── main.go ├── go.mod ├── LICENSE └── go.sum /.github/workflows/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tetratelabs/platform-owners -------------------------------------------------------------------------------- /.licenserignore: -------------------------------------------------------------------------------- 1 | docs/examples/**/*.tf 2 | examples/**/*.tf 3 | .goreleaser.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | dist/ 3 | terraform-provider-checkmate 4 | terraform.tfstate* 5 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "checkmate" { 2 | # no configuration required 3 | } 4 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "checkmate Provider" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # checkmate Provider 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | provider "checkmate" { 17 | # no configuration required 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. 4 | 5 | The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. 6 | 7 | * **provider/provider.tf** example file for the provider index page 8 | * **data-sources/`full data source name`/data-source.tf** example file for the named data source page 9 | * **resources/`full resource name`/resource.tf** example file for the named data source page 10 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build tools 16 | 17 | package tools 18 | 19 | import ( 20 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pkg 16 | 17 | import ( 18 | framework "github.com/hashicorp/terraform-plugin-framework/provider" 19 | 20 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/provider" 21 | ) 22 | 23 | var version string = "1.6.0" 24 | 25 | func NewProvider() framework.Provider { 26 | return provider.New(version)() 27 | } 28 | -------------------------------------------------------------------------------- /examples/resources/checkmate_local_command/resource.tf: -------------------------------------------------------------------------------- 1 | resource "checkmate_local_command" "example" { 2 | # Run this command in a shell 3 | command = "python3 $CHECKMATE_FILEPATH" 4 | 5 | # Switch to this directory before running the command 6 | working_directory = "./scripts" 7 | 8 | # The overall test should not take longer than 5 seconds 9 | timeout = 5000 10 | 11 | # Wait 0.1 seconds between attempts 12 | interval = 100 13 | 14 | # We want 2 successes in a row 15 | consecutive_successes = 2 16 | 17 | # Create the script file before running the attempts 18 | create_file = { 19 | name = "fancy_script.py" 20 | contents = "print('hello world')" 21 | use_working_dir = true 22 | create_directory = true 23 | } 24 | 25 | create_anyway_on_check_failure = false 26 | } 27 | 28 | output "stdout" { 29 | value = checkmate_local_command.example.stdout 30 | } 31 | 32 | output "stderr" { 33 | value = checkmate_local_command.example.stderr 34 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Tetrate 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | default: build 16 | 17 | # Run acceptance tests 18 | .PHONY: test 19 | test: 20 | TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m 21 | 22 | licenser: 23 | licenser apply Tetrate -r 24 | 25 | build: 26 | go build -v ./... 27 | 28 | install: 29 | go install 30 | 31 | format: 32 | go fmt ./... 33 | 34 | docs: install 35 | go generate ./... 36 | 37 | check: docs licenser format 38 | [ -z "`git status -uno --porcelain`" ] || (git status && echo 'Check failed. This could be a failed check or dirty git state.'; exit 1) -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | branches: [ "*" ] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.21 16 | - name: Install licenser 17 | run: 'go install github.com/liamawhite/licenser@v0.7.0' 18 | - uses: hashicorp/setup-terraform@v3 19 | with: 20 | terraform_version: "1.10.4" 21 | - name: Verify licenses and docs 22 | run: make check 23 | 24 | build: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Set up Go 30 | uses: actions/setup-go@v3 31 | with: 32 | go-version: 1.21 33 | 34 | - name: Build 35 | run: make build 36 | 37 | test: 38 | runs-on: ubuntu-latest 39 | services: 40 | httpbin: 41 | image: kennethreitz/httpbin 42 | ports: [ "80:80" ] 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: Set up Go 47 | uses: actions/setup-go@v3 48 | with: 49 | go-version: 1.21 50 | 51 | - name: Build 52 | run: make test 53 | env: 54 | HTTPBIN: "http://localhost" 55 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Checkmate 2 | 3 | Make "readiness" a resource 4 | 5 | # Overview 6 | 7 | Sometimes resources you create with Terraform might not be ready yet but their associated provider reports them as ready, or a resource you depend on is created outside of the awareness of Terraform. This provider aims to bridge that gap and allow you to create "virtual" resources that represent "readiness checkpoints" in your Terraform dependency tree. 8 | 9 | ## Requirements 10 | 11 | - [Terraform](https://www.terraform.io/downloads.html) >= 1.0 12 | - [Go](https://golang.org/doc/install) >= 1.18 13 | 14 | ## Building The Provider 15 | 16 | 1. Clone the repository 17 | 1. Enter the repository directory 18 | 1. Build the provider using the Go `install` command: 19 | 20 | ```shell 21 | go install 22 | ``` 23 | 24 | ## Using the provider 25 | 26 | ## Developing the Provider 27 | 28 | If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (see [Requirements](#requirements) above). 29 | 30 | To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. 31 | 32 | To generate or update documentation, run `go generate`. 33 | 34 | In order to run the full suite of Acceptance tests, run `make testacc`. 35 | 36 | ```shell 37 | make test 38 | ``` 39 | -------------------------------------------------------------------------------- /pkg/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 21 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 22 | ) 23 | 24 | // testAccProtoV6ProviderFactories are used to instantiate a provider during 25 | // acceptance testing. The factory function will be invoked for every Terraform 26 | // CLI command executed to create a provider server to which the CLI can 27 | // reattach. 28 | var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ 29 | "checkmate": providerserver.NewProtocol6WithError(New("test")()), 30 | } 31 | 32 | func testAccPreCheck(t *testing.T) { 33 | // You can add code here to run prior to any test case execution, for example assertions 34 | // about the appropriate environment variables being set are common to see in a pre-check 35 | // function. 36 | } 37 | -------------------------------------------------------------------------------- /examples/resources/checkmate_tcp_echo/resource.tf: -------------------------------------------------------------------------------- 1 | resource "checkmate_tcp_echo" "example" { 2 | # The hostname where the echo request will be sent 3 | host = "foo.bar" 4 | 5 | # The TCP port at which the request will be sent 6 | port = 3002 7 | 8 | # Message that will be sent to the TCP echo server 9 | message = "PROXY tcpbin.com:4242 foobartest" 10 | 11 | # Message expected to be present in the echo response 12 | expected_message = "foobartest" 13 | 14 | # Set the connection timeout for the destination host, in milliseconds 15 | connection_timeout = 3000 16 | 17 | # Set the per try timeout for the destination host, in milliseconds 18 | single_attempt_timeout = 2000 19 | 20 | # Set a number of consecutive sucesses to make the check pass 21 | consecutive_successes = 5 22 | } 23 | 24 | # In case you expect to be some kind of problem, and not getting 25 | # a response back, you can set `expect_failure` to true. In that case 26 | # you can skip `expected_message`. 27 | resource "checkmate_tcp_echo" "example" { 28 | # The hostname where the echo request will be sent 29 | host = "foo.bar" 30 | 31 | # The TCP port at which the request will be sent 32 | port = 3002 33 | 34 | # Message that will be sent to the TCP echo server 35 | message = "PROXY nonexistent.local:4242 foobartest" 36 | 37 | # Expect this to fail 38 | expect_write_failure = true 39 | 40 | # Set the connection timeout for the destination host, in milliseconds 41 | connection_timeout = 3000 42 | 43 | # Set the per try timeout for the destination host, in milliseconds 44 | single_attempt_timeout = 2000 45 | 46 | # Set a number of consecutive sucesses to make the check pass 47 | consecutive_successes = 5 48 | } 49 | -------------------------------------------------------------------------------- /pkg/modifiers/default_int64.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package modifiers 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 22 | "github.com/hashicorp/terraform-plugin-framework/types" 23 | ) 24 | 25 | var _ planmodifier.Int64 = defaultInt64Modifier{} 26 | 27 | func DefaultInt64(def int64) defaultInt64Modifier { 28 | return defaultInt64Modifier{Default: types.Int64Value(def)} 29 | } 30 | 31 | type defaultInt64Modifier struct { 32 | Default types.Int64 33 | } 34 | 35 | // PlanModifyInt64 implements planmodifier.Int64 36 | func (m defaultInt64Modifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { 37 | if !req.ConfigValue.IsNull() { 38 | return 39 | } 40 | 41 | resp.PlanValue = m.Default 42 | } 43 | 44 | func (m defaultInt64Modifier) String() string { 45 | return m.Default.String() 46 | } 47 | 48 | func (m defaultInt64Modifier) Description(ctx context.Context) string { 49 | return fmt.Sprintf("If value is not configured, defaults to `%s`", m) 50 | } 51 | 52 | func (m defaultInt64Modifier) MarkdownDescription(ctx context.Context) string { 53 | return fmt.Sprintf("If value is not configured, defaults to `%s`", m) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/helpers/retry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package helpers 16 | 17 | import ( 18 | "context" 19 | "time" 20 | ) 21 | 22 | type RetryWindow struct { 23 | Context context.Context 24 | Timeout time.Duration 25 | Interval time.Duration 26 | ConsecutiveSuccesses int 27 | } 28 | 29 | type RetryResult int 30 | 31 | const ( 32 | Success RetryResult = iota 33 | TimeoutExceeded 34 | Failure 35 | ) 36 | 37 | func (r *RetryWindow) Do(action func(attempt int, successes int) bool) RetryResult { 38 | success := make(chan struct{}) 39 | failure := make(chan struct{}) 40 | go func() { 41 | attempt := 0 42 | successCount := 0 43 | // run a while true loop, exiting when the timeout expires 44 | for { 45 | select { 46 | case <-r.Context.Done(): 47 | failure <- struct{}{} 48 | return 49 | default: 50 | attempt++ 51 | if action(attempt, successCount) { 52 | successCount++ 53 | if successCount >= r.ConsecutiveSuccesses { 54 | success <- struct{}{} 55 | return 56 | } 57 | } else { 58 | successCount = 0 59 | } 60 | time.Sleep(r.Interval) 61 | } 62 | } 63 | }() 64 | 65 | select { 66 | case <-success: 67 | return Success 68 | case <-failure: 69 | return Failure 70 | case <-time.After(r.Timeout): 71 | return TimeoutExceeded 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub action can publish assets for release when a tag is created. 2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 3 | # 4 | # This uses an action (hashicorp/ghaction-import-gpg) that assumes you set your 5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 6 | # secret. If you would rather own your own GPG handling, please fork this action 7 | # or use an alternative one for key handling. 8 | # 9 | # You will need to pass the `--batch` flag to `gpg` in your signing step 10 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 11 | # 12 | name: release 13 | on: 14 | push: 15 | tags: 16 | - 'v*' 17 | permissions: 18 | contents: write 19 | jobs: 20 | goreleaser: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - 24 | name: Checkout 25 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 26 | - 27 | name: Unshallow 28 | run: git fetch --prune --unshallow 29 | - 30 | name: Set up Go 31 | uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 32 | with: 33 | go-version-file: 'go.mod' 34 | cache: true 35 | - 36 | name: Import GPG key 37 | uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0 38 | id: import_gpg 39 | with: 40 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 41 | passphrase: ${{ secrets.PASSPHRASE }} 42 | - 43 | name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0 45 | with: 46 | version: latest 47 | args: release --clean 48 | env: 49 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 50 | # GitHub sets this automatically 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | # this is just an example and not a requirement for provider building/publishing 5 | - go mod tidy 6 | builds: 7 | - env: 8 | # goreleaser does not work with CGO, it could also complicate 9 | # usage by users in CI/CD systems like Terraform Cloud where 10 | # they are unable to install libraries. 11 | - CGO_ENABLED=0 12 | mod_timestamp: "{{ .CommitTimestamp }}" 13 | flags: 14 | - -trimpath 15 | ldflags: 16 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}" 17 | goos: 18 | - freebsd 19 | - windows 20 | - linux 21 | - darwin 22 | goarch: 23 | - amd64 24 | - "386" 25 | - arm 26 | - arm64 27 | ignore: 28 | - goos: darwin 29 | goarch: "386" 30 | binary: "{{ .ProjectName }}_v{{ .Version }}" 31 | archives: 32 | - format: zip 33 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 34 | checksum: 35 | extra_files: 36 | - glob: "terraform-registry-manifest.json" 37 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 38 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 39 | algorithm: sha256 40 | signs: 41 | - artifacts: checksum 42 | args: 43 | # if you are using this in a GitHub action or some other automated pipeline, you 44 | # need to pass the batch flag to indicate its not interactive. 45 | - "--batch" 46 | - "--local-user" 47 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 48 | - "--output" 49 | - "${signature}" 50 | - "--detach-sign" 51 | - "${artifact}" 52 | release: 53 | extra_files: 54 | - glob: "terraform-registry-manifest.json" 55 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 56 | # If you want to manually examine the release before its live, uncomment this line: 57 | # draft: true 58 | changelog: 59 | disable: true 60 | -------------------------------------------------------------------------------- /pkg/healthcheck/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package healthcheck 16 | 17 | import ( 18 | "context" 19 | "net/http" 20 | "net/http/httptest" 21 | "testing" 22 | ) 23 | 24 | func TestHealthCheck(t *testing.T) { 25 | // TODO: add some cases that validate the mutated data and diagnostic parameters. 26 | tests := []struct { 27 | name string 28 | args *HttpHealthArgs 29 | mock func(w http.ResponseWriter, r *http.Request) 30 | wantErr bool 31 | }{ 32 | { 33 | name: "errors on timeout", 34 | args: &HttpHealthArgs{ 35 | Method: "GET", 36 | Timeout: 1000, 37 | ConsecutiveSuccesses: 2, 38 | StatusCode: "200", 39 | JSONPath: "{.SomeField}", 40 | JSONValue: "someValue", 41 | }, 42 | mock: func(w http.ResponseWriter, r *http.Request) { 43 | w.WriteHeader(http.StatusOK) 44 | w.Write([]byte(`{"SomeField": "notSomeValue"}`)) 45 | }, 46 | wantErr: true, 47 | }, 48 | } 49 | 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | server := httptest.NewServer(http.HandlerFunc(tt.mock)) 53 | defer server.Close() 54 | tt.args.URL = server.URL 55 | 56 | err := HealthCheck(context.Background(), tt.args, nil) 57 | if (err != nil) != tt.wantErr { 58 | t.Errorf("HealthCheck() error = %v, wantErr %v", err, tt.wantErr) 59 | } 60 | }) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /pkg/modifiers/default_string.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package modifiers 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 22 | "github.com/hashicorp/terraform-plugin-framework/types" 23 | ) 24 | 25 | var _ planmodifier.String = defaultStringModifier{} 26 | 27 | func DefaultString(def string) defaultStringModifier { 28 | return defaultStringModifier{Default: types.StringValue(def)} 29 | } 30 | 31 | func NullableString() defaultStringModifier { 32 | return defaultStringModifier{Default: types.StringNull()} 33 | } 34 | 35 | type defaultStringModifier struct { 36 | Default types.String 37 | } 38 | 39 | // PlanModifyString implements planmodifier.String 40 | func (m defaultStringModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { 41 | if !req.ConfigValue.IsNull() { 42 | return 43 | } 44 | 45 | resp.PlanValue = m.Default 46 | } 47 | 48 | func (m defaultStringModifier) String() string { 49 | return m.Default.String() 50 | } 51 | 52 | func (m defaultStringModifier) Description(ctx context.Context) string { 53 | return fmt.Sprintf("If value is not configured, defaults to `%s`", m) 54 | } 55 | 56 | func (m defaultStringModifier) MarkdownDescription(ctx context.Context) string { 57 | return fmt.Sprintf("If value is not configured, defaults to `%s`", m) 58 | } 59 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "log" 21 | 22 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 23 | 24 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/provider" 25 | ) 26 | 27 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 28 | 29 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 30 | // ensure the documentation is formatted properly. 31 | //go:generate terraform fmt -recursive ./examples/ 32 | 33 | // Run the docs generation tool, check its repository for more information on how it works and how docs 34 | // can be customized. 35 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 36 | 37 | var ( 38 | // these will be set by the goreleaser configuration 39 | // to appropriate values for the compiled binary 40 | version string = "1.6.0" 41 | 42 | // goreleaser can also pass the specific commit if you want 43 | // commit string = "" 44 | ) 45 | 46 | func main() { 47 | var debug bool 48 | 49 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 50 | flag.Parse() 51 | 52 | opts := providerserver.ServeOpts{ 53 | // TODO: Update this string with the published name of your provider. 54 | Address: "registry.terraform.io/tetratelabs/checkmate", 55 | Debug: debug, 56 | } 57 | 58 | err := providerserver.Serve(context.Background(), provider.New(version), opts) 59 | 60 | if err != nil { 61 | log.Fatal(err.Error()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/resources/checkmate_http_health/resource.tf: -------------------------------------------------------------------------------- 1 | resource "checkmate_http_health" "example" { 2 | # This is the url of the endpoint we want to check 3 | url = "http://example.com" 4 | 5 | # Will perform an HTTP GET request 6 | method = "GET" 7 | 8 | # The overall test should not take longer than 10 seconds 9 | timeout = 10000 10 | 11 | # Wait 0.1 seconds between attempts 12 | interval = 100 13 | 14 | # Expect a status 200 OK 15 | status_code = 200 16 | 17 | # We want 2 successes in a row 18 | consecutive_successes = 2 19 | 20 | # Send these HTTP headers 21 | headers = { 22 | "Example-Header" = "example value" 23 | } 24 | } 25 | 26 | resource "checkmate_http_health" "example_ca_bundle" { 27 | url = "https://untrusted-root.badssl.com/" 28 | method = "GET" 29 | interval = 1 30 | status_code = 200 31 | consecutive_successes = 2 32 | ca_bundle = file("badssl-root.cert.cer") 33 | } 34 | 35 | resource "checkmate_http_health" "example_no_ca_bundle" { 36 | url = "https://httpbin.org/status/200" 37 | request_timeout = 1000 38 | method = "GET" 39 | interval = 1 40 | status_code = 200 41 | consecutive_successes = 2 42 | } 43 | 44 | resource "checkmate_http_health" "example_insecure_tls" { 45 | url = "https://self-signed.badssl.com/" 46 | request_timeout = 1000 47 | method = "GET" 48 | interval = 1 49 | status_code = 200 50 | consecutive_successes = 2 51 | insecure_tls = true 52 | } 53 | 54 | resource "checkmate_http_health" "example_json_path" { 55 | url = "https://httpbin.org/headers" 56 | request_timeout = 1000 57 | method = "GET" 58 | interval = 1 59 | status_code = 200 60 | consecutive_successes = 2 61 | jsonpath = "{ .Host }" 62 | json_value = "httpbin.org" 63 | } 64 | 65 | resource "checkmate_http_health" "example_json_path_regex" { 66 | url = "https://httpbin.org/headers" 67 | request_timeout = 1000 68 | method = "GET" 69 | interval = 1 70 | status_code = 200 71 | consecutive_successes = 2 72 | jsonpath = "{ .User-Agent }" 73 | json_value = "curl/.*" 74 | } 75 | -------------------------------------------------------------------------------- /pkg/provider/resource_local_command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 22 | ) 23 | 24 | func TestAccLocalCommandResource(t *testing.T) { 25 | resource.Test(t, resource.TestCase{ 26 | PreCheck: func() { testAccPreCheck(t) }, 27 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 28 | Steps: []resource.TestStep{ 29 | { 30 | Config: testAccLocalCommandResourceConfig("test_success", "true", false), 31 | Check: resource.ComposeAggregateTestCheckFunc( 32 | resource.TestCheckResourceAttr("checkmate_local_command.test_success", "passed", "true"), 33 | ), 34 | }, 35 | { 36 | Config: testAccLocalCommandResourceConfig("test_failure", "false", true), 37 | Check: resource.ComposeAggregateTestCheckFunc( 38 | resource.TestCheckResourceAttr("checkmate_local_command.test_failure", "passed", "false"), 39 | ), 40 | }, 41 | { 42 | Config: testAccLocalCommandResourceCreateFileConfig("test_file"), 43 | Check: resource.ComposeAggregateTestCheckFunc( 44 | resource.TestCheckResourceAttr("checkmate_local_command.test_file", "passed", "true"), 45 | resource.TestCheckResourceAttr("checkmate_local_command.test_file", "stdout", "hello world"), 46 | ), 47 | }, 48 | }, 49 | }) 50 | } 51 | 52 | func testAccLocalCommandResourceConfig(name string, command string, ignore_failure bool) string { 53 | return fmt.Sprintf(` 54 | resource "checkmate_local_command" %[1]q { 55 | command = %[2]q 56 | timeout = 1000 57 | create_anyway_on_check_failure = %[3]t 58 | }`, name, command, ignore_failure) 59 | 60 | } 61 | 62 | func testAccLocalCommandResourceCreateFileConfig(name string) string { 63 | return fmt.Sprintf(` 64 | resource "checkmate_local_command" %[1]q { 65 | command = "cat $CHECKMATE_FILEPATH" 66 | timeout = 1000 67 | create_file = { 68 | name = "testing_create_file" 69 | contents = "hello world" 70 | } 71 | }`, name) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/provider/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/hashicorp/terraform-plugin-framework/datasource" 21 | "github.com/hashicorp/terraform-plugin-framework/provider" 22 | "github.com/hashicorp/terraform-plugin-framework/provider/schema" 23 | "github.com/hashicorp/terraform-plugin-framework/resource" 24 | ) 25 | 26 | // Ensure provider satisfies various provider interfaces. 27 | var _ provider.Provider = &CheckmateProvider{} 28 | 29 | // CheckmateProvider defines the provider implementation. 30 | type CheckmateProvider struct { 31 | // version is set to the provider version on release, "dev" when the 32 | // provider is built and ran locally, and "test" when running acceptance 33 | // testing. 34 | version string 35 | } 36 | 37 | // Schema implements provider.Provider 38 | func (*CheckmateProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { 39 | resp.Schema = schema.Schema{ 40 | Attributes: map[string]schema.Attribute{}, 41 | } 42 | } 43 | 44 | // CheckProviderModel describes the provider data model. 45 | type CheckProviderModel struct{} 46 | 47 | func (p *CheckmateProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { 48 | resp.TypeName = "checkmate" 49 | resp.Version = p.version 50 | } 51 | 52 | func (p *CheckmateProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { 53 | } 54 | 55 | func (p *CheckmateProvider) Resources(ctx context.Context) []func() resource.Resource { 56 | return []func() resource.Resource{ 57 | NewHttpHealthResource, 58 | NewLocalCommandResource, 59 | NewTCPEchoResource, 60 | } 61 | } 62 | 63 | func (p *CheckmateProvider) DataSources(ctx context.Context) []func() datasource.DataSource { 64 | return []func() datasource.DataSource{} 65 | } 66 | 67 | func New(version string) func() provider.Provider { 68 | return func() provider.Provider { 69 | return &CheckmateProvider{ 70 | version: version, 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/provider/resource_tcp_echo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 22 | ) 23 | 24 | func TestAccTCPEchoResource(t *testing.T) { 25 | resource.Test(t, resource.TestCase{ 26 | PreCheck: func() { testAccPreCheck(t) }, 27 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 28 | Steps: []resource.TestStep{ 29 | { 30 | Config: testAccTCPEchoResourceConfig("test_success", "tcpbin.com", 4242, "foobar", "foobar", false), 31 | Check: resource.ComposeAggregateTestCheckFunc( 32 | resource.TestCheckResourceAttr("checkmate_tcp_echo.test_success", "passed", "true"), 33 | ), 34 | }, 35 | { 36 | Config: testAccTCPEchoResourceConfig("test_failure", "foo.bar", 1234, "foobar", "foobar", true), 37 | Check: resource.ComposeAggregateTestCheckFunc( 38 | resource.TestCheckResourceAttr("checkmate_tcp_echo.test_failure", "passed", "false"), 39 | ), 40 | }, 41 | { 42 | Config: testAccTCPEchoResourceConfig("test_failure", "foo.bar", 1234, "foobar", "foobar", true), 43 | Check: resource.ComposeAggregateTestCheckFunc( 44 | resource.TestCheckResourceAttr("checkmate_tcp_echo.test_failure", "passed", "false"), 45 | ), 46 | }, 47 | { 48 | Config: testTCPEchoResourceRegex("test_regex_ok", `\(.*\)`, false), 49 | Check: resource.ComposeAggregateTestCheckFunc( 50 | resource.TestCheckResourceAttr("checkmate_tcp_echo.test_regex_ok", "passed", "true"), 51 | ), 52 | }, 53 | { 54 | Config: testTCPEchoResourceRegex("test_regex_not_match_pass_anyway", "test", true), 55 | Check: resource.ComposeAggregateTestCheckFunc( 56 | resource.TestCheckResourceAttr("checkmate_tcp_echo.test_regex_not_match_pass_anyway", "passed", "false"), 57 | ), 58 | }, 59 | }, 60 | }) 61 | } 62 | 63 | func testAccTCPEchoResourceConfig(name, host string, port int, message, expected_message string, ignore_failure bool) string { 64 | return fmt.Sprintf(` 65 | resource "checkmate_tcp_echo" %q { 66 | host = %q 67 | port = %d 68 | message = %q 69 | timeout = 1000 70 | expected_message = %q 71 | create_anyway_on_check_failure = %t 72 | }`, name, host, port, message, expected_message, ignore_failure) 73 | 74 | } 75 | 76 | func testTCPEchoResourceRegex(name, regex string, ignore_failure bool) string { 77 | return fmt.Sprintf(` 78 | resource "checkmate_tcp_echo" %q { 79 | host = "tcpbin.platform.tetrate.com" 80 | port = 15080 81 | message = "foobar (123)" 82 | timeout = 1000 * 10 83 | expected_message = "foobar (123)" 84 | persistent_response_regex = %q 85 | create_anyway_on_check_failure = %t 86 | consecutive_successes = 2 87 | }`, name, regex, ignore_failure) 88 | 89 | } 90 | -------------------------------------------------------------------------------- /docs/resources/local_command.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "checkmate_local_command Resource - terraform-provider-checkmate" 4 | subcategory: "" 5 | description: |- 6 | Local Command 7 | --- 8 | 9 | # checkmate_local_command (Resource) 10 | 11 | Local Command 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "checkmate_local_command" "example" { 17 | # Run this command in a shell 18 | command = "python3 $CHECKMATE_FILEPATH" 19 | 20 | # Switch to this directory before running the command 21 | working_directory = "./scripts" 22 | 23 | # The overall test should not take longer than 5 seconds 24 | timeout = 5000 25 | 26 | # Wait 0.1 seconds between attempts 27 | interval = 100 28 | 29 | # We want 2 successes in a row 30 | consecutive_successes = 2 31 | 32 | # Create the script file before running the attempts 33 | create_file = { 34 | name = "fancy_script.py" 35 | contents = "print('hello world')" 36 | use_working_dir = true 37 | create_directory = true 38 | } 39 | 40 | create_anyway_on_check_failure = false 41 | } 42 | 43 | output "stdout" { 44 | value = checkmate_local_command.example.stdout 45 | } 46 | 47 | output "stderr" { 48 | value = checkmate_local_command.example.stderr 49 | } 50 | ``` 51 | 52 | 53 | ## Schema 54 | 55 | ### Required 56 | 57 | - `command` (String) The command to run (passed to `sh -c`) 58 | 59 | ### Optional 60 | 61 | - `command_timeout` (Number) Timeout for an individual attempt. If exceeded, the attempt will be considered failure and potentially retried. Default 5000ms 62 | - `consecutive_successes` (Number) Number of consecutive successes required before the check is considered successful overall. Defaults to 1. 63 | - `create_anyway_on_check_failure` (Boolean) If false, the resource will fail to create if the check does not pass. If true, the resource will be created anyway. Defaults to false. 64 | - `create_file` (Attributes) Ensure a file exists with the following contents. The path to this file will be available in the env var CHECKMATE_FILEPATH (see [below for nested schema](#nestedatt--create_file)) 65 | - `env` (Map of String) Map of environment variables to apply to the command. Inherits the parent environment 66 | - `interval` (Number) Interval in milliseconds between attemps. Default 200 67 | - `keepers` (Map of String) Arbitrary map of string values that when changed will cause the check to run again. 68 | - `timeout` (Number) Overall timeout in milliseconds for the check before giving up, default 10000 69 | - `working_directory` (String) Working directory where the command will be run. Defaults to the current working directory 70 | 71 | ### Read-Only 72 | 73 | - `id` (String) Identifier 74 | - `passed` (Boolean) True if the check passed 75 | - `stderr` (String) Standard error output of the command 76 | - `stdout` (String) Standard output of the command 77 | 78 | 79 | ### Nested Schema for `create_file` 80 | 81 | Required: 82 | 83 | - `contents` (String) Contents of the file to create 84 | - `name` (String) Name of the created file. 85 | 86 | Optional: 87 | 88 | - `create_directory` (Boolean) Create the target directory if it doesn't exist. Defaults to false. 89 | - `use_working_dir` (Boolean) If true, will use the working directory instead of a temporary directory. Defaults to false. 90 | 91 | Read-Only: 92 | 93 | - `path` (String) Path to the file that was created 94 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/terraform-provider-checkmate 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/google/uuid v1.5.0 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/hashicorp/terraform-plugin-docs v0.16.0 9 | github.com/hashicorp/terraform-plugin-framework v1.4.2 10 | github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 11 | github.com/hashicorp/terraform-plugin-go v0.19.1 12 | github.com/hashicorp/terraform-plugin-log v0.9.0 13 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 14 | k8s.io/client-go v0.29.2 15 | ) 16 | 17 | require ( 18 | github.com/Masterminds/goutils v1.1.1 // indirect 19 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 20 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect 21 | github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect 22 | github.com/agext/levenshtein v1.2.2 // indirect 23 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 24 | github.com/armon/go-radix v1.0.0 // indirect 25 | github.com/bgentry/speakeasy v0.1.0 // indirect 26 | github.com/cloudflare/circl v1.3.7 // indirect 27 | github.com/fatih/color v1.13.0 // indirect 28 | github.com/golang/protobuf v1.5.3 // indirect 29 | github.com/google/go-cmp v0.6.0 // 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.4.1-0.20200414143053-d3edf31b6320 // indirect 34 | github.com/hashicorp/go-hclog v1.5.0 // indirect 35 | github.com/hashicorp/go-plugin v1.5.2 // indirect 36 | github.com/hashicorp/go-uuid v1.0.3 // indirect 37 | github.com/hashicorp/go-version v1.6.0 // indirect 38 | github.com/hashicorp/hc-install v0.6.1 // indirect 39 | github.com/hashicorp/hcl/v2 v2.19.1 // indirect 40 | github.com/hashicorp/logutils v1.0.0 // indirect 41 | github.com/hashicorp/terraform-exec v0.19.0 // indirect 42 | github.com/hashicorp/terraform-json v0.17.1 // indirect 43 | github.com/hashicorp/terraform-registry-address v0.2.3 // indirect 44 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 45 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect 46 | github.com/huandu/xstrings v1.3.2 // indirect 47 | github.com/imdario/mergo v0.3.15 // indirect 48 | github.com/mattn/go-colorable v0.1.13 // indirect 49 | github.com/mattn/go-isatty v0.0.16 // indirect 50 | github.com/mitchellh/cli v1.1.5 // indirect 51 | github.com/mitchellh/copystructure v1.2.0 // indirect 52 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 53 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 54 | github.com/mitchellh/mapstructure v1.5.0 // indirect 55 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 56 | github.com/oklog/run v1.0.0 // indirect 57 | github.com/posener/complete v1.2.3 // indirect 58 | github.com/russross/blackfriday v1.6.0 // indirect 59 | github.com/shopspring/decimal v1.3.1 // indirect 60 | github.com/spf13/cast v1.5.0 // indirect 61 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 62 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 63 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 64 | github.com/zclconf/go-cty v1.14.1 // indirect 65 | golang.org/x/crypto v0.20.0 // indirect 66 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect 67 | golang.org/x/mod v0.13.0 // indirect 68 | golang.org/x/net v0.21.0 // indirect 69 | golang.org/x/sys v0.17.0 // indirect 70 | golang.org/x/text v0.14.0 // indirect 71 | google.golang.org/appengine v1.6.7 // indirect 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 73 | google.golang.org/grpc v1.59.0 // indirect 74 | google.golang.org/protobuf v1.31.0 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /docs/resources/tcp_echo.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "checkmate_tcp_echo Resource - terraform-provider-checkmate" 4 | subcategory: "" 5 | description: |- 6 | TCP Echo 7 | --- 8 | 9 | # checkmate_tcp_echo (Resource) 10 | 11 | TCP Echo 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "checkmate_tcp_echo" "example" { 17 | # The hostname where the echo request will be sent 18 | host = "foo.bar" 19 | 20 | # The TCP port at which the request will be sent 21 | port = 3002 22 | 23 | # Message that will be sent to the TCP echo server 24 | message = "PROXY tcpbin.com:4242 foobartest" 25 | 26 | # Message expected to be present in the echo response 27 | expected_message = "foobartest" 28 | 29 | # Set the connection timeout for the destination host, in milliseconds 30 | connection_timeout = 3000 31 | 32 | # Set the per try timeout for the destination host, in milliseconds 33 | single_attempt_timeout = 2000 34 | 35 | # Set a number of consecutive sucesses to make the check pass 36 | consecutive_successes = 5 37 | } 38 | 39 | # In case you expect to be some kind of problem, and not getting 40 | # a response back, you can set `expect_failure` to true. In that case 41 | # you can skip `expected_message`. 42 | resource "checkmate_tcp_echo" "example" { 43 | # The hostname where the echo request will be sent 44 | host = "foo.bar" 45 | 46 | # The TCP port at which the request will be sent 47 | port = 3002 48 | 49 | # Message that will be sent to the TCP echo server 50 | message = "PROXY nonexistent.local:4242 foobartest" 51 | 52 | # Expect this to fail 53 | expect_write_failure = true 54 | 55 | # Set the connection timeout for the destination host, in milliseconds 56 | connection_timeout = 3000 57 | 58 | # Set the per try timeout for the destination host, in milliseconds 59 | single_attempt_timeout = 2000 60 | 61 | # Set a number of consecutive sucesses to make the check pass 62 | consecutive_successes = 5 63 | } 64 | ``` 65 | 66 | 67 | ## Schema 68 | 69 | ### Required 70 | 71 | - `host` (String) The hostname where to send the TCP echo request to 72 | - `message` (String) The message to send in the echo request 73 | - `port` (Number) The port of the hostname where to send the TCP echo request 74 | 75 | ### Optional 76 | 77 | - `connection_timeout` (Number) The timeout for stablishing a new TCP connection in milliseconds 78 | - `consecutive_successes` (Number) Number of consecutive successes required before the check is considered successful overall. Defaults to 1. 79 | - `create_anyway_on_check_failure` (Boolean) If false, the resource will fail to create if the check does not pass. If true, the resource will be created anyway. Defaults to false. 80 | - `expect_write_failure` (Boolean) Wether or not the check is expected to fail after successfully connecting to the target. If true, the check will be considered successful if it fails. Defaults to false. 81 | - `expected_message` (String) The message expected to be included in the echo response 82 | - `interval` (Number) Interval in milliseconds between attemps. Default 200 83 | - `keepers` (Map of String) Arbitrary map of string values that when changed will cause the check to run again. 84 | - `persistent_response_regex` (String) A regex pattern that the response need to match in every attempt to be considered successful. 85 | If not provided, the response is not checked. 86 | 87 | If using multiple attempts, this regex will be evaulated against the response text. For every susequent attempt, the regex 88 | will be evaluated against the response text and compared against the first obtained value. The check will be deemed successful 89 | if the regex matches the response text in every attempt. A single response not matching such value will cause the check to fail. 90 | - `single_attempt_timeout` (Number) Timeout for an individual attempt. If exceeded, the attempt will be considered failure and potentially retried. Default 5000ms 91 | - `timeout` (Number) Overall timeout in milliseconds for the check before giving up, default 10000 92 | 93 | ### Read-Only 94 | 95 | - `id` (String) Identifier 96 | - `passed` (Boolean) True if the check passed 97 | -------------------------------------------------------------------------------- /docs/resources/http_health.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "checkmate_http_health Resource - terraform-provider-checkmate" 4 | subcategory: "" 5 | description: |- 6 | HTTPS Healthcheck 7 | --- 8 | 9 | # checkmate_http_health (Resource) 10 | 11 | HTTPS Healthcheck 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "checkmate_http_health" "example" { 17 | # This is the url of the endpoint we want to check 18 | url = "http://example.com" 19 | 20 | # Will perform an HTTP GET request 21 | method = "GET" 22 | 23 | # The overall test should not take longer than 10 seconds 24 | timeout = 10000 25 | 26 | # Wait 0.1 seconds between attempts 27 | interval = 100 28 | 29 | # Expect a status 200 OK 30 | status_code = 200 31 | 32 | # We want 2 successes in a row 33 | consecutive_successes = 2 34 | 35 | # Send these HTTP headers 36 | headers = { 37 | "Example-Header" = "example value" 38 | } 39 | } 40 | 41 | resource "checkmate_http_health" "example_ca_bundle" { 42 | url = "https://untrusted-root.badssl.com/" 43 | method = "GET" 44 | interval = 1 45 | status_code = 200 46 | consecutive_successes = 2 47 | ca_bundle = file("badssl-root.cert.cer") 48 | } 49 | 50 | resource "checkmate_http_health" "example_no_ca_bundle" { 51 | url = "https://httpbin.org/status/200" 52 | request_timeout = 1000 53 | method = "GET" 54 | interval = 1 55 | status_code = 200 56 | consecutive_successes = 2 57 | } 58 | 59 | resource "checkmate_http_health" "example_insecure_tls" { 60 | url = "https://self-signed.badssl.com/" 61 | request_timeout = 1000 62 | method = "GET" 63 | interval = 1 64 | status_code = 200 65 | consecutive_successes = 2 66 | insecure_tls = true 67 | } 68 | 69 | resource "checkmate_http_health" "example_json_path" { 70 | url = "https://httpbin.org/headers" 71 | request_timeout = 1000 72 | method = "GET" 73 | interval = 1 74 | status_code = 200 75 | consecutive_successes = 2 76 | jsonpath = "{ .Host }" 77 | json_value = "httpbin.org" 78 | } 79 | 80 | resource "checkmate_http_health" "example_json_path_regex" { 81 | url = "https://httpbin.org/headers" 82 | request_timeout = 1000 83 | method = "GET" 84 | interval = 1 85 | status_code = 200 86 | consecutive_successes = 2 87 | jsonpath = "{ .User-Agent }" 88 | json_value = "curl/.*" 89 | } 90 | ``` 91 | 92 | 93 | ## Schema 94 | 95 | ### Required 96 | 97 | - `url` (String) URL 98 | 99 | ### Optional 100 | 101 | - `ca_bundle` (String) The CA bundle to use when connecting to the target host. 102 | - `consecutive_successes` (Number) Number of consecutive successes required before the check is considered successful overall. Defaults to 1. 103 | - `create_anyway_on_check_failure` (Boolean) If false, the resource will fail to create if the check does not pass. If true, the resource will be created anyway. Defaults to false. 104 | - `headers` (Map of String) HTTP Request Headers 105 | - `insecure_tls` (Boolean) Wether or not to completely skip the TLS CA verification. Default false. 106 | - `interval` (Number) Interval in milliseconds between attemps. Default 200 107 | - `json_value` (String) Optional regular expression to apply to the result of the JSONPath expression. If the expression matches, the check will pass. 108 | - `jsonpath` (String) Optional JSONPath expression (same syntax as kubectl jsonpath output) to apply to the result body. If the expression matches, the check will pass. 109 | - `keepers` (Map of String) Arbitrary map of string values that when changed will cause the healthcheck to run again. 110 | - `method` (String) HTTP Method, defaults to GET 111 | - `request_body` (String) Optional request body to send on each attempt. 112 | - `request_timeout` (Number) Timeout for an individual request. If exceeded, the attempt will be considered failure and potentially retried. Default 1000 113 | - `status_code` (String) Status Code to expect. Can be a comma seperated list of ranges like '100-200,500'. Default 200 114 | - `timeout` (Number) Overall timeout in milliseconds for the check before giving up. Default 5000 115 | 116 | ### Read-Only 117 | 118 | - `id` (String) Identifier 119 | - `passed` (Boolean) True if the check passed 120 | - `result_body` (String) Result body 121 | -------------------------------------------------------------------------------- /pkg/provider/resource_http_health_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | "testing" 22 | 23 | "github.com/hashicorp/terraform-plugin-framework/diag" 24 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 25 | ) 26 | 27 | func TestAccHttpHealthResource(t *testing.T) { 28 | timeout := 6000 // 6s 29 | httpBin, envExists := os.LookupEnv("HTTPBIN") 30 | if !envExists { 31 | httpBin = "https://httpbin.platform.tetrate.com" 32 | } 33 | url200 := httpBin + "/status/200" 34 | urlPost := httpBin + "/post" 35 | urlHeaders := httpBin + "/headers" 36 | 37 | resource.Test(t, resource.TestCase{ 38 | PreCheck: func() { testAccPreCheck(t) }, 39 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 40 | Steps: []resource.TestStep{ 41 | // Create and Read testing 42 | { 43 | Config: testAccHttpHealthResourceConfig("test", url200, timeout), 44 | Check: resource.ComposeAggregateTestCheckFunc( 45 | resource.TestCheckResourceAttr("checkmate_http_health.test", "url", url200), 46 | ), 47 | }, 48 | { 49 | Config: testAccHttpHealthResourceConfig("test_headers", urlHeaders, timeout), 50 | Check: resource.ComposeAggregateTestCheckFunc( 51 | resource.TestCheckResourceAttrWith("checkmate_http_health.test_headers", "result_body", checkHeader("Hello", "world")), 52 | ), 53 | }, 54 | { 55 | Config: testAccHttpHealthResourceConfigWithBody("test_post", urlPost, "hello", timeout), 56 | Check: resource.ComposeAggregateTestCheckFunc( 57 | resource.TestCheckResourceAttrWith("checkmate_http_health.test_post", "result_body", checkResponse("hello")), 58 | ), 59 | }, 60 | { 61 | Config: testJSONPath("test_jp", "https://httpbin.platform.tetrate.com/headers", "{ .headers.Host }", "httpbin.platform.tetrate.com"), 62 | Check: resource.ComposeAggregateTestCheckFunc( 63 | resource.TestCheckResourceAttr("checkmate_http_health.test_jp", "passed", "true"), 64 | ), 65 | }, 66 | { 67 | Config: testJSONPath("test_jp_re", urlHeaders, "{ .headers.User-Agent }", "Go-(http|https)-client.*"), 68 | Check: resource.ComposeAggregateTestCheckFunc( 69 | resource.TestCheckResourceAttr("checkmate_http_health.test_jp_re", "passed", "true"), 70 | ), 71 | }, 72 | }, 73 | }) 74 | } 75 | 76 | func TestStatusCodePattern(t *testing.T) { 77 | tests := []struct { 78 | pattern string 79 | code int 80 | want bool 81 | wantErr bool 82 | }{ 83 | {"200", 200, true, false}, 84 | {"200-204,300-305", 204, true, false}, 85 | {"200-204,300-305", 299, false, false}, 86 | {"foo", 200, false, true}, 87 | {"200-204,204-300", 200, true, false}, 88 | {"200-204-300", 200, false, true}, 89 | {"200,,", 0, false, true}, 90 | {"--200", 0, false, true}, 91 | } 92 | for _, tt := range tests { 93 | diag := &diag.Diagnostics{} 94 | got := checkStatusCode(tt.pattern, tt.code, diag) 95 | if got != tt.want { 96 | t.Errorf("checkStatusCode(%q, %d) got %v, want %v", tt.pattern, tt.code, got, tt.want) 97 | } 98 | if tt.wantErr { 99 | if !diag.HasError() { 100 | t.Errorf("checkStatusCode(%q, %d) expected an error, but got none", tt.pattern, tt.code) 101 | } 102 | } else { 103 | if diag.HasError() { 104 | t.Errorf("checkStatusCode(%q, %d) got unexpected errors: %v", tt.pattern, tt.code, diag) 105 | } 106 | } 107 | } 108 | } 109 | 110 | func testAccHttpHealthResourceConfig(name string, url string, timeout int) string { 111 | return fmt.Sprintf(` 112 | resource "checkmate_http_health" %[1]q { 113 | url = %[2]q 114 | consecutive_successes = 1 115 | headers = { 116 | hello = "world" 117 | } 118 | timeout = %[3]d 119 | } 120 | `, name, url, timeout) 121 | } 122 | 123 | func testAccHttpHealthResourceConfigWithBody(name string, url string, body string, timeout int) string { 124 | return fmt.Sprintf(` 125 | resource "checkmate_http_health" %[1]q { 126 | url = %[2]q 127 | consecutive_successes = 1 128 | method = "POST" 129 | headers = { 130 | "Content-Type" = "application/text" 131 | } 132 | request_body = %[3]q 133 | timeout = %[4]d 134 | } 135 | `, name, url, body, timeout) 136 | 137 | } 138 | 139 | func testJSONPath(name string, url, jsonpath, json_value string) string { 140 | return fmt.Sprintf(` 141 | resource "checkmate_http_health" %[1]q { 142 | url = %[2]q 143 | consecutive_successes = 1 144 | method = "GET" 145 | timeout = 1000 * 10 146 | interval = 1000 * 2 147 | jsonpath = %[3]q 148 | json_value = %[4]q 149 | } 150 | `, name, url, jsonpath, json_value) 151 | 152 | } 153 | 154 | func checkHeader(key string, value string) func(string) error { 155 | return func(responseBody string) error { 156 | var parsed map[string]map[string]string 157 | if err := json.Unmarshal([]byte(responseBody), &parsed); err != nil { 158 | return err 159 | } 160 | if val, ok := parsed["headers"][key]; ok { 161 | if val == value { 162 | return nil 163 | } 164 | return fmt.Errorf("Key %q exists but value %q does not match", key, val) 165 | } 166 | return fmt.Errorf("Key %q does not exist in returned headers", key) 167 | } 168 | } 169 | 170 | func checkResponse(value string) func(string) error { 171 | return func(responseBody string) error { 172 | var parsed map[string]interface{} 173 | if err := json.Unmarshal([]byte(responseBody), &parsed); err != nil { 174 | return err 175 | } 176 | if val, ok := parsed["data"]; ok { 177 | if val == value { 178 | return nil 179 | } 180 | return fmt.Errorf("Value returned %q does not match %q", parsed["data"], val) 181 | } 182 | return fmt.Errorf("Bad response from httpbin") 183 | } 184 | } 185 | 186 | func checkPassed(value string) error { 187 | if value == "true" { 188 | return nil 189 | } 190 | return fmt.Errorf("test did not pass") 191 | } 192 | -------------------------------------------------------------------------------- /pkg/healthcheck/http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package healthcheck 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "crypto/tls" 21 | "crypto/x509" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "net/http" 27 | "net/url" 28 | "regexp" 29 | "strconv" 30 | "strings" 31 | "time" 32 | 33 | "github.com/hashicorp/go-multierror" 34 | "github.com/hashicorp/terraform-plugin-framework/diag" 35 | "github.com/hashicorp/terraform-plugin-log/tflog" 36 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/helpers" 37 | "k8s.io/client-go/util/jsonpath" 38 | ) 39 | 40 | type HttpHealthArgs struct { 41 | URL string 42 | Method string 43 | Timeout int64 44 | RequestTimeout int64 45 | Interval int64 46 | StatusCode string 47 | ConsecutiveSuccesses int64 48 | Headers map[string]string 49 | IgnoreFailure bool 50 | Passed bool 51 | RequestBody string 52 | ResultBody string 53 | CABundle string 54 | InsecureTLS bool 55 | JSONPath string 56 | JSONValue string 57 | } 58 | 59 | func HealthCheck(ctx context.Context, data *HttpHealthArgs, diag *diag.Diagnostics) error { 60 | var err error 61 | 62 | data.Passed = false 63 | endpoint, err := url.Parse(data.URL) 64 | if err != nil { 65 | diagAddError(diag, "Client Error", fmt.Sprintf("Unable to parse url %q, got error %s", data.URL, err)) 66 | return fmt.Errorf("parse url %q: %w", data.URL, err) 67 | } 68 | 69 | if (data.JSONPath != "" && data.JSONValue == "") || (data.JSONPath == "" && data.JSONValue != "") { 70 | diagAddError(diag, "Client Error", "Both JSONPath and JSONValue must be specified") 71 | return errors.New("both JSONPath and JSONValue must be specified") 72 | } 73 | 74 | var checkCode func(int) (bool, error) 75 | // check the pattern once 76 | _, err = checkStatusCode(data.StatusCode, 0, diag) 77 | if err != nil { 78 | return fmt.Errorf("bad status code pattern: %w", err) 79 | } 80 | checkCode = func(c int) (bool, error) { return checkStatusCode(data.StatusCode, c, diag) } 81 | 82 | // normalize headers 83 | headers := make(map[string][]string) 84 | if data.Headers != nil { 85 | for k, v := range data.Headers { 86 | headers[k] = []string{v} 87 | } 88 | } 89 | 90 | window := helpers.RetryWindow{ 91 | Context: ctx, 92 | Timeout: time.Duration(data.Timeout) * time.Millisecond, 93 | Interval: time.Duration(data.Interval) * time.Millisecond, 94 | ConsecutiveSuccesses: int(data.ConsecutiveSuccesses), 95 | } 96 | data.ResultBody = "" 97 | 98 | if data.CABundle != "" && data.InsecureTLS { 99 | diagAddError(diag, "Conflicting configuration", "You cannot specify both custom CA and insecure TLS. Please use only one of them.") 100 | } 101 | tlsConfig := &tls.Config{} 102 | if data.CABundle != "" { 103 | caCertPool := x509.NewCertPool() 104 | if ok := caCertPool.AppendCertsFromPEM([]byte(data.CABundle)); !ok { 105 | diagAddError(diag, "Building CA cert pool", err.Error()) 106 | multierror.Append(err, fmt.Errorf("build CA cert pool: %w", err)) 107 | } 108 | tlsConfig.RootCAs = caCertPool 109 | } 110 | tlsConfig.InsecureSkipVerify = data.InsecureTLS 111 | 112 | client := http.Client{ 113 | Transport: &http.Transport{ 114 | TLSClientConfig: tlsConfig, 115 | ForceAttemptHTTP2: true, 116 | }, 117 | Timeout: time.Duration(data.RequestTimeout) * time.Millisecond, 118 | } 119 | 120 | tflog.Debug(ctx, fmt.Sprintf("Starting HTTP health check. Overall timeout: %d ms, request timeout: %d ms", data.Timeout, data.RequestTimeout)) 121 | for h, v := range headers { 122 | tflog.Debug(ctx, fmt.Sprintf("%s: %s", h, v)) 123 | } 124 | 125 | result := window.Do(func(attempt int, successes int) bool { 126 | if successes != 0 { 127 | tflog.Trace(ctx, fmt.Sprintf("SUCCESS [%d/%d] http %s %s", successes, data.ConsecutiveSuccesses, data.Method, endpoint)) 128 | } else { 129 | tflog.Trace(ctx, fmt.Sprintf("ATTEMPT #%d http %s %s", attempt, data.Method, endpoint)) 130 | } 131 | 132 | httpResponse, err := client.Do(&http.Request{ 133 | URL: endpoint, 134 | Method: data.Method, 135 | Header: headers, 136 | Body: io.NopCloser(strings.NewReader(data.RequestBody)), 137 | }) 138 | if err != nil { 139 | tflog.Warn(ctx, fmt.Sprintf("CONNECTION FAILURE %v", err)) 140 | return false 141 | } 142 | 143 | success, err := checkCode(httpResponse.StatusCode) 144 | if err != nil { 145 | diagAddError(diag, "check status code", err.Error()) 146 | multierror.Append(err, fmt.Errorf("check status code: %w", err)) 147 | } 148 | if success { 149 | tflog.Trace(ctx, fmt.Sprintf("SUCCESS CODE %d", httpResponse.StatusCode)) 150 | body, err := io.ReadAll(httpResponse.Body) 151 | if err != nil { 152 | tflog.Warn(ctx, fmt.Sprintf("ERROR READING BODY %v", err)) 153 | data.ResultBody = "" 154 | } else { 155 | tflog.Warn(ctx, fmt.Sprintf("READ %d BYTES", len(body))) 156 | data.ResultBody = string(body) 157 | } 158 | } else { 159 | tflog.Trace(ctx, fmt.Sprintf("FAILURE CODE %d", httpResponse.StatusCode)) 160 | } 161 | 162 | // Check JSONPath 163 | if data.JSONPath != "" && data.JSONValue != "" { 164 | j := jsonpath.New("parser") 165 | err = j.Parse(data.JSONPath) 166 | if err != nil { 167 | tflog.Warn(ctx, fmt.Sprintf("ERROR PARSING JSONPATH EXPRESSION %v", err)) 168 | return false 169 | } 170 | var respJSON interface{} 171 | err = json.Unmarshal([]byte(data.ResultBody), &respJSON) 172 | if err != nil { 173 | tflog.Warn(ctx, fmt.Sprintf("ERROR UNMARSHALLING JSON %v", err)) 174 | return false 175 | } 176 | buf := new(bytes.Buffer) 177 | err = j.Execute(buf, respJSON) 178 | if err != nil { 179 | tflog.Warn(ctx, fmt.Sprintf("ERROR EXECUTING JSONPATH %v", err)) 180 | return false 181 | } 182 | re := regexp.MustCompile(data.JSONValue) 183 | return re.MatchString(buf.String()) 184 | } 185 | 186 | return success 187 | }) 188 | 189 | switch result { 190 | case helpers.Success: 191 | data.Passed = true 192 | case helpers.TimeoutExceeded: 193 | diagAddWarning(diag, "Timeout exceeded", fmt.Sprintf("Timeout of %d milliseconds exceeded", data.Timeout)) 194 | if !data.IgnoreFailure { 195 | diagAddError(diag, "Check failed", "The check did not pass within the timeout and create_anyway_on_check_failure is false") 196 | err = multierror.Append(err, fmt.Errorf("the check did not pass within the timeout and create_anyway_on_check_failure is false")) 197 | } 198 | } 199 | 200 | return err 201 | } 202 | 203 | func checkStatusCode(pattern string, code int, diag *diag.Diagnostics) (bool, error) { 204 | ranges := strings.Split(pattern, ",") 205 | for _, r := range ranges { 206 | bounds := strings.Split(r, "-") 207 | if len(bounds) == 2 { 208 | left, err := strconv.Atoi(bounds[0]) 209 | if err != nil { 210 | diagAddError(diag, "Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[0], err)) 211 | return false, fmt.Errorf("convert %q to integer: %w", bounds[0], err) 212 | } 213 | right, err := strconv.Atoi(bounds[1]) 214 | if err != nil { 215 | diagAddError(diag, "Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[1], err)) 216 | return false, fmt.Errorf("convert %q to integer: %w", bounds[0], err) 217 | } 218 | if left > right { 219 | diagAddError(diag, "Bad status code pattern", fmt.Sprintf("Left bound %d is greater than right bound %d", left, right)) 220 | return false, fmt.Errorf("left bound %d is greater than right bound %d", left, right) 221 | } 222 | if left <= code && right >= code { 223 | return true, nil 224 | } 225 | } else if len(bounds) == 1 { 226 | val, err := strconv.Atoi(bounds[0]) 227 | if err != nil { 228 | diagAddError(diag, "Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[0], err)) 229 | return false, fmt.Errorf("convert %q to integer: %w", bounds[0], err) 230 | } 231 | if val == code { 232 | return true, nil 233 | } 234 | } else { 235 | diagAddError(diag, "Bad status code pattern", "Too many dashes in range pattern") 236 | return false, errors.New("too many dashes in range pattern") 237 | } 238 | } 239 | return false, nil 240 | } 241 | 242 | func diagAddError(diag *diag.Diagnostics, summary string, details string) { 243 | if diag != nil { 244 | diag.AddError(summary, details) 245 | } 246 | } 247 | 248 | func diagAddWarning(diag *diag.Diagnostics, summary string, details string) { 249 | if diag != nil { 250 | diag.AddWarning(summary, details) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Tetrate 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkg/provider/resource_http_health.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strconv" 21 | "strings" 22 | 23 | "github.com/google/uuid" 24 | "github.com/hashicorp/terraform-plugin-framework/diag" 25 | "github.com/hashicorp/terraform-plugin-framework/path" 26 | "github.com/hashicorp/terraform-plugin-framework/resource" 27 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 28 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 29 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 30 | "github.com/hashicorp/terraform-plugin-framework/types" 31 | 32 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/healthcheck" 33 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/modifiers" 34 | ) 35 | 36 | // Ensure provider defined types fully satisfy framework interfaces 37 | var _ resource.Resource = &HttpHealthResource{} 38 | var _ resource.ResourceWithImportState = &HttpHealthResource{} 39 | 40 | func NewHttpHealthResource() resource.Resource { 41 | return &HttpHealthResource{} 42 | } 43 | 44 | type HttpHealthResource struct{} 45 | 46 | // Schema implements resource.Resource 47 | func (*HttpHealthResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 48 | resp.Schema = schema.Schema{ 49 | MarkdownDescription: "HTTPS Healthcheck", 50 | 51 | Attributes: map[string]schema.Attribute{ 52 | "url": schema.StringAttribute{ 53 | MarkdownDescription: "URL", 54 | Required: true, 55 | }, 56 | "method": schema.StringAttribute{ 57 | MarkdownDescription: "HTTP Method, defaults to GET", 58 | Optional: true, 59 | Computed: true, 60 | PlanModifiers: []planmodifier.String{modifiers.DefaultString("GET")}, 61 | }, 62 | "timeout": schema.Int64Attribute{ 63 | MarkdownDescription: "Overall timeout in milliseconds for the check before giving up. Default 5000", 64 | Optional: true, 65 | Computed: true, 66 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(5000)}, 67 | }, 68 | "request_timeout": schema.Int64Attribute{ 69 | MarkdownDescription: "Timeout for an individual request. If exceeded, the attempt will be considered failure and potentially retried. Default 1000", 70 | Optional: true, 71 | Computed: true, 72 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(1000)}, 73 | }, 74 | "interval": schema.Int64Attribute{ 75 | MarkdownDescription: "Interval in milliseconds between attemps. Default 200", 76 | Optional: true, 77 | Computed: true, 78 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(200)}, 79 | }, 80 | "status_code": schema.StringAttribute{ 81 | MarkdownDescription: "Status Code to expect. Can be a comma seperated list of ranges like '100-200,500'. Default 200", 82 | Optional: true, 83 | Computed: true, 84 | PlanModifiers: []planmodifier.String{modifiers.DefaultString("200")}, 85 | }, 86 | "consecutive_successes": schema.Int64Attribute{ 87 | MarkdownDescription: "Number of consecutive successes required before the check is considered successful overall. Defaults to 1.", 88 | Optional: true, 89 | Computed: true, 90 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(1)}, 91 | }, 92 | "headers": schema.MapAttribute{ 93 | ElementType: types.StringType, 94 | MarkdownDescription: "HTTP Request Headers", 95 | Optional: true, 96 | }, 97 | "id": schema.StringAttribute{ 98 | Computed: true, 99 | MarkdownDescription: "Identifier", 100 | PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, 101 | }, 102 | "request_body": schema.StringAttribute{ 103 | MarkdownDescription: "Optional request body to send on each attempt.", 104 | Optional: true, 105 | }, 106 | "result_body": schema.StringAttribute{ 107 | Computed: true, 108 | MarkdownDescription: "Result body", 109 | }, 110 | "passed": schema.BoolAttribute{ 111 | Computed: true, 112 | MarkdownDescription: "True if the check passed", 113 | }, 114 | "create_anyway_on_check_failure": schema.BoolAttribute{ 115 | Optional: true, 116 | MarkdownDescription: "If false, the resource will fail to create if the check does not pass. If true, the resource will be created anyway. Defaults to false.", 117 | }, 118 | "ca_bundle": schema.StringAttribute{ 119 | Optional: true, 120 | MarkdownDescription: "The CA bundle to use when connecting to the target host.", 121 | }, 122 | "insecure_tls": schema.BoolAttribute{ 123 | Optional: true, 124 | MarkdownDescription: "Wether or not to completely skip the TLS CA verification. Default false.", 125 | }, 126 | "jsonpath": schema.StringAttribute{ 127 | Optional: true, 128 | MarkdownDescription: "Optional JSONPath expression (same syntax as kubectl jsonpath output) to apply to the result body. If the expression matches, the check will pass.", 129 | }, 130 | "json_value": schema.StringAttribute{ 131 | Optional: true, 132 | MarkdownDescription: "Optional regular expression to apply to the result of the JSONPath expression. If the expression matches, the check will pass.", 133 | }, 134 | "keepers": schema.MapAttribute{ 135 | ElementType: types.StringType, 136 | MarkdownDescription: "Arbitrary map of string values that when changed will cause the healthcheck to run again.", 137 | Optional: true, 138 | }, 139 | }, 140 | } 141 | } 142 | 143 | type HttpHealthResourceModel struct { 144 | URL types.String `tfsdk:"url"` 145 | Id types.String `tfsdk:"id"` 146 | Method types.String `tfsdk:"method"` 147 | Timeout types.Int64 `tfsdk:"timeout"` 148 | RequestTimeout types.Int64 `tfsdk:"request_timeout"` 149 | Interval types.Int64 `tfsdk:"interval"` 150 | StatusCode types.String `tfsdk:"status_code"` 151 | ConsecutiveSuccesses types.Int64 `tfsdk:"consecutive_successes"` 152 | Headers types.Map `tfsdk:"headers"` 153 | IgnoreFailure types.Bool `tfsdk:"create_anyway_on_check_failure"` 154 | Passed types.Bool `tfsdk:"passed"` 155 | RequestBody types.String `tfsdk:"request_body"` 156 | ResultBody types.String `tfsdk:"result_body"` 157 | CABundle types.String `tfsdk:"ca_bundle"` 158 | InsecureTLS types.Bool `tfsdk:"insecure_tls"` 159 | Keepers types.Map `tfsdk:"keepers"` 160 | JSONPath types.String `tfsdk:"jsonpath"` 161 | JSONValue types.String `tfsdk:"json_value"` 162 | } 163 | 164 | func (r *HttpHealthResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 165 | resp.TypeName = req.ProviderTypeName + "_http_health" 166 | } 167 | 168 | func (r *HttpHealthResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 169 | var data HttpHealthResourceModel 170 | 171 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 172 | if resp.Diagnostics.HasError() { 173 | return 174 | } 175 | 176 | data.Id = types.StringValue(uuid.NewString()) 177 | 178 | r.HealthCheck(ctx, &data, &resp.Diagnostics) 179 | if resp.Diagnostics.HasError() { 180 | return 181 | } 182 | 183 | resp.Diagnostics.Append(resp.State.Set(ctx, data)...) 184 | 185 | } 186 | 187 | func (r *HttpHealthResource) HealthCheck(ctx context.Context, data *HttpHealthResourceModel, diag *diag.Diagnostics) { 188 | var tmp map[string]string 189 | if !data.Headers.IsNull() { 190 | diag.Append(data.Headers.ElementsAs(ctx, &tmp, false)...) 191 | } 192 | args := healthcheck.HttpHealthArgs{ 193 | URL: data.URL.ValueString(), 194 | Method: data.Method.ValueString(), 195 | Timeout: data.Timeout.ValueInt64(), 196 | RequestTimeout: data.RequestTimeout.ValueInt64(), 197 | Interval: data.Interval.ValueInt64(), 198 | StatusCode: data.StatusCode.ValueString(), 199 | ConsecutiveSuccesses: data.ConsecutiveSuccesses.ValueInt64(), 200 | Headers: tmp, 201 | IgnoreFailure: data.IgnoreFailure.ValueBool(), 202 | RequestBody: data.RequestBody.ValueString(), 203 | CABundle: data.CABundle.ValueString(), 204 | InsecureTLS: data.InsecureTLS.ValueBool(), 205 | JSONPath: data.JSONPath.ValueString(), 206 | JSONValue: data.JSONValue.ValueString(), 207 | } 208 | 209 | err := healthcheck.HealthCheck(ctx, &args, diag) 210 | if err != nil { 211 | diag.AddError("Health Check Error", fmt.Sprintf("Error during health check: %s", err)) 212 | } 213 | 214 | data.Passed = types.BoolValue(args.Passed) 215 | data.ResultBody = types.StringValue(args.ResultBody) 216 | } 217 | 218 | func (r *HttpHealthResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 219 | var data HttpHealthResourceModel 220 | 221 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 222 | 223 | if resp.Diagnostics.HasError() { 224 | return 225 | } 226 | 227 | resp.Diagnostics.Append(resp.State.Set(ctx, data)...) 228 | } 229 | 230 | func (r *HttpHealthResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 231 | var data HttpHealthResourceModel 232 | 233 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 234 | if resp.Diagnostics.HasError() { 235 | return 236 | } 237 | 238 | r.HealthCheck(ctx, &data, &resp.Diagnostics) 239 | if resp.Diagnostics.HasError() { 240 | return 241 | } 242 | 243 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 244 | } 245 | 246 | func (r *HttpHealthResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 247 | } 248 | 249 | func (r *HttpHealthResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 250 | resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) 251 | } 252 | 253 | func checkStatusCode(pattern string, code int, diag *diag.Diagnostics) bool { 254 | ranges := strings.Split(pattern, ",") 255 | for _, r := range ranges { 256 | bounds := strings.Split(r, "-") 257 | if len(bounds) == 2 { 258 | left, err := strconv.Atoi(bounds[0]) 259 | if err != nil { 260 | diag.AddError("Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[0], err)) 261 | return false 262 | } 263 | right, err := strconv.Atoi(bounds[1]) 264 | if err != nil { 265 | diag.AddError("Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[1], err)) 266 | return false 267 | } 268 | if left > right { 269 | diag.AddError("Bad status code pattern", fmt.Sprintf("Left bound %d is greater than right bound %d", left, right)) 270 | return false 271 | } 272 | if left <= code && right >= code { 273 | return true 274 | } 275 | } else if len(bounds) == 1 { 276 | val, err := strconv.Atoi(bounds[0]) 277 | if err != nil { 278 | diag.AddError("Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[0], err)) 279 | return false 280 | } 281 | if val == code { 282 | return true 283 | } 284 | } else { 285 | diag.AddError("Bad status code pattern", "Too many dashes in range pattern") 286 | return false 287 | } 288 | } 289 | return false 290 | } 291 | -------------------------------------------------------------------------------- /pkg/provider/resource_tcp_echo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "net" 22 | "regexp" 23 | "strconv" 24 | "strings" 25 | "time" 26 | 27 | "github.com/google/uuid" 28 | "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" 29 | "github.com/hashicorp/terraform-plugin-framework/diag" 30 | "github.com/hashicorp/terraform-plugin-framework/path" 31 | "github.com/hashicorp/terraform-plugin-framework/resource" 32 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 33 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" 34 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 35 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" 36 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 37 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 38 | "github.com/hashicorp/terraform-plugin-framework/types" 39 | "github.com/hashicorp/terraform-plugin-log/tflog" 40 | 41 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/helpers" 42 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/modifiers" 43 | ) 44 | 45 | var _ resource.Resource = &TCPEchoResource{} 46 | var _ resource.ResourceWithImportState = &TCPEchoResource{} 47 | 48 | type TCPEchoResource struct{} 49 | 50 | // Schema implements resource.Resource 51 | func (*TCPEchoResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 52 | resp.Schema = schema.Schema{ 53 | MarkdownDescription: "TCP Echo", 54 | 55 | Attributes: map[string]schema.Attribute{ 56 | "host": schema.StringAttribute{ 57 | MarkdownDescription: "The hostname where to send the TCP echo request to", 58 | Required: true, 59 | }, 60 | "port": schema.Int64Attribute{ 61 | MarkdownDescription: "The port of the hostname where to send the TCP echo request", 62 | Required: true, 63 | Validators: []validator.Int64{ 64 | int64validator.Between(1, 65535), 65 | }, 66 | }, 67 | "message": schema.StringAttribute{ 68 | MarkdownDescription: "The message to send in the echo request", 69 | Required: true, 70 | }, 71 | "expected_message": schema.StringAttribute{ 72 | MarkdownDescription: "The message expected to be included in the echo response", 73 | Required: false, 74 | Optional: true, 75 | Computed: true, 76 | Default: stringdefault.StaticString(""), 77 | }, 78 | "persistent_response_regex": schema.StringAttribute{ 79 | MarkdownDescription: `A regex pattern that the response need to match in every attempt to be considered successful. 80 | If not provided, the response is not checked. 81 | 82 | If using multiple attempts, this regex will be evaulated against the response text. For every susequent attempt, the regex 83 | will be evaluated against the response text and compared against the first obtained value. The check will be deemed successful 84 | if the regex matches the response text in every attempt. A single response not matching such value will cause the check to fail.`, 85 | Required: false, 86 | Optional: true, 87 | Computed: true, 88 | Default: stringdefault.StaticString(""), 89 | }, 90 | "expect_write_failure": schema.BoolAttribute{ 91 | MarkdownDescription: "Wether or not the check is expected to fail after successfully connecting to the target. If true, the check will be considered successful if it fails. Defaults to false.", 92 | Required: false, 93 | Optional: true, 94 | Computed: true, 95 | Default: booldefault.StaticBool(false), 96 | }, 97 | "timeout": schema.Int64Attribute{ 98 | MarkdownDescription: "Overall timeout in milliseconds for the check before giving up, default 10000", 99 | Optional: true, 100 | Computed: true, 101 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(10000)}, 102 | }, 103 | "connection_timeout": schema.Int64Attribute{ 104 | MarkdownDescription: "The timeout for stablishing a new TCP connection in milliseconds", 105 | Optional: true, 106 | Computed: true, 107 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(5000)}, 108 | }, 109 | "single_attempt_timeout": schema.Int64Attribute{ 110 | MarkdownDescription: "Timeout for an individual attempt. If exceeded, the attempt will be considered failure and potentially retried. Default 5000ms", 111 | Optional: true, 112 | Computed: true, 113 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(5000)}, 114 | }, 115 | "interval": schema.Int64Attribute{ 116 | MarkdownDescription: "Interval in milliseconds between attemps. Default 200", 117 | Optional: true, 118 | Computed: true, 119 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(200)}, 120 | }, 121 | "consecutive_successes": schema.Int64Attribute{ 122 | MarkdownDescription: "Number of consecutive successes required before the check is considered successful overall. Defaults to 1.", 123 | Optional: true, 124 | Computed: true, 125 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(1)}, 126 | }, 127 | "passed": schema.BoolAttribute{ 128 | Computed: true, 129 | MarkdownDescription: "True if the check passed", 130 | }, 131 | "create_anyway_on_check_failure": schema.BoolAttribute{ 132 | Optional: true, 133 | MarkdownDescription: "If false, the resource will fail to create if the check does not pass. If true, the resource will be created anyway. Defaults to false.", 134 | }, 135 | "id": schema.StringAttribute{ 136 | Computed: true, 137 | MarkdownDescription: "Identifier", 138 | PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, 139 | }, 140 | "keepers": schema.MapAttribute{ 141 | ElementType: types.StringType, 142 | MarkdownDescription: "Arbitrary map of string values that when changed will cause the check to run again.", 143 | Optional: true, 144 | }, 145 | }} 146 | } 147 | 148 | type TCPEchoResourceModel struct { 149 | Id types.String `tfsdk:"id"` 150 | Host types.String `tfsdk:"host"` 151 | Port types.Int64 `tfsdk:"port"` 152 | Message types.String `tfsdk:"message"` 153 | ExpectedMessage types.String `tfsdk:"expected_message"` 154 | PersistentResponseRegex types.String `tfsdk:"persistent_response_regex"` 155 | ExpectWriteFailure types.Bool `tfsdk:"expect_write_failure"` 156 | ConnectionTimeout types.Int64 `tfsdk:"connection_timeout"` 157 | SingleAttemptTimeout types.Int64 `tfsdk:"single_attempt_timeout"` 158 | Timeout types.Int64 `tfsdk:"timeout"` 159 | Interval types.Int64 `tfsdk:"interval"` 160 | ConsecutiveSuccesses types.Int64 `tfsdk:"consecutive_successes"` 161 | IgnoreFailure types.Bool `tfsdk:"create_anyway_on_check_failure"` 162 | Passed types.Bool `tfsdk:"passed"` 163 | Keepers types.Map `tfsdk:"keepers"` 164 | } 165 | 166 | // ImportState implements resource.ResourceWithImportState 167 | func (*TCPEchoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 168 | resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) 169 | } 170 | 171 | // Create implements resource.Resource 172 | func (r *TCPEchoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 173 | var data TCPEchoResourceModel 174 | 175 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 176 | if resp.Diagnostics.HasError() { 177 | return 178 | } 179 | 180 | data.Id = types.StringValue(uuid.NewString()) 181 | 182 | r.TCPEcho(ctx, &data, &resp.Diagnostics) 183 | if resp.Diagnostics.HasError() { 184 | return 185 | } 186 | 187 | resp.Diagnostics.Append(resp.State.Set(ctx, data)...) 188 | } 189 | 190 | func (r *TCPEchoResource) TCPEcho(ctx context.Context, data *TCPEchoResourceModel, diag *diag.Diagnostics) { 191 | if !data.ExpectWriteFailure.ValueBool() && data.ExpectedMessage.ValueString() == "" { 192 | tflog.Error(ctx, "expected_message is required when expect_failure is false") 193 | return 194 | } 195 | if data.ExpectedMessage.ValueString() != "" && data.ExpectWriteFailure.ValueBool() { 196 | tflog.Warn(ctx, "expected_message is ignored when expect_failure is true") 197 | } 198 | 199 | data.Passed = types.BoolValue(false) 200 | 201 | window := helpers.RetryWindow{ 202 | Context: ctx, 203 | Timeout: time.Duration(data.Timeout.ValueInt64()) * time.Millisecond, 204 | Interval: time.Duration(data.Interval.ValueInt64()) * time.Millisecond, 205 | ConsecutiveSuccesses: int(data.ConsecutiveSuccesses.ValueInt64()), 206 | } 207 | 208 | previousRegexValue := "" 209 | var persistentResponseRegex *regexp.Regexp 210 | var err error 211 | if data.PersistentResponseRegex.ValueString() != "" { 212 | persistentResponseRegex, err = regexp.Compile(data.PersistentResponseRegex.ValueString()) 213 | if err != nil { 214 | tflog.Error(ctx, fmt.Sprintf("could not compile regex %q: %v", data.PersistentResponseRegex.ValueString(), err.Error())) 215 | diag.AddError("Invalid regex", fmt.Sprintf("Could not compile regex %q: %v", data.PersistentResponseRegex.ValueString(), err.Error())) 216 | return 217 | } 218 | } 219 | 220 | result := window.Do(func(attempt int, success int) bool { 221 | exepctFailure := data.ExpectWriteFailure.ValueBool() 222 | destStr := data.Host.ValueString() + ":" + strconv.Itoa(int(data.Port.ValueInt64())) 223 | 224 | d := net.Dialer{Timeout: time.Duration(data.ConnectionTimeout.ValueInt64()) * time.Millisecond} 225 | conn, err := d.Dial("tcp", destStr) 226 | if err != nil { 227 | tflog.Warn(ctx, fmt.Sprintf("dial %q failed: %v", destStr, err.Error())) 228 | return false 229 | } 230 | defer conn.Close() 231 | 232 | _, err = conn.Write([]byte(data.Message.ValueString() + "\n")) 233 | if err != nil { 234 | tflog.Warn(ctx, fmt.Sprintf("write to server failed: %v", err.Error())) 235 | return false 236 | } 237 | 238 | deadlineDuration := time.Millisecond * time.Duration(data.SingleAttemptTimeout.ValueInt64()) 239 | err = conn.SetDeadline(time.Now().Add(deadlineDuration)) 240 | if err != nil && !exepctFailure { 241 | tflog.Warn(ctx, fmt.Sprintf("could not set connection deadline: %v", err.Error())) 242 | return false 243 | } 244 | 245 | reply := make([]byte, 1024) 246 | _, err = conn.Read(reply) 247 | if err != nil { 248 | if exepctFailure { 249 | // We expected this 250 | return true 251 | } 252 | tflog.Warn(ctx, fmt.Sprintf("read from server failed: %v", err.Error())) 253 | return false 254 | } 255 | // At this point, if we expect failure, we can just return the check failed, 256 | // as we were expecting it to fail 257 | if exepctFailure { 258 | return false 259 | } 260 | 261 | // remove null char from response 262 | reply = bytes.Trim(reply, "\x00") 263 | 264 | if persistentResponseRegex != nil { 265 | limits := persistentResponseRegex.FindStringIndex(string(reply)) 266 | if limits == nil { 267 | tflog.Warn(ctx, fmt.Sprintf("Got response %q, which does not match regex %q", string(reply), data.PersistentResponseRegex.ValueString())) 268 | diag.AddWarning("Check failed", fmt.Sprintf("Got response %q, which does not match regex %q", string(reply), data.PersistentResponseRegex.ValueString())) 269 | return false 270 | } 271 | result := string(reply)[limits[0]:limits[1]] 272 | // result := persistentResponseRegex.FindString(string(reply)) 273 | tflog.Info(ctx, fmt.Sprintf("Result: %s", result)) 274 | 275 | if previousRegexValue != result { 276 | tflog.Warn(ctx, fmt.Sprintf("Got response %q, which does not match previous attempt %q", result, previousRegexValue)) 277 | diag.AddWarning("Check failed", fmt.Sprintf("Got response %q, which does not match previous attempt %q", result, previousRegexValue)) 278 | 279 | previousRegexValue = result 280 | return false 281 | } 282 | } 283 | 284 | if !strings.Contains(string(reply), data.ExpectedMessage.ValueString()) { 285 | tflog.Warn(ctx, fmt.Sprintf("Got response %q, which does not include expected message %q", string(reply), data.ExpectedMessage.ValueString())) 286 | diag.AddWarning("Check failed", fmt.Sprintf("Got response %q, which does not include expected message %q", string(reply), data.ExpectedMessage.ValueString())) 287 | return false 288 | } 289 | 290 | return true 291 | }) 292 | 293 | switch result { 294 | case helpers.Success: 295 | data.Passed = types.BoolValue(true) 296 | case helpers.TimeoutExceeded: 297 | diag.AddWarning("Timeout exceeded", fmt.Sprintf("Timeout of %d milliseconds exceeded", data.Timeout.ValueInt64())) 298 | if !data.IgnoreFailure.ValueBool() { 299 | diag.AddError("Check failed", "The check did not pass and create_anyway_on_check_failure is false") 300 | return 301 | } 302 | } 303 | 304 | } 305 | 306 | // Delete implements resource.Resource 307 | func (*TCPEchoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 308 | var data *TCPEchoResourceModel 309 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 310 | 311 | if resp.Diagnostics.HasError() { 312 | return 313 | } 314 | } 315 | 316 | // Metadata implements resource.Resource 317 | func (*TCPEchoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 318 | resp.TypeName = req.ProviderTypeName + "_tcp_echo" 319 | } 320 | 321 | // Read implements resource.Resource 322 | func (*TCPEchoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 323 | var data *TCPEchoResourceModel 324 | 325 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 326 | if resp.Diagnostics.HasError() { 327 | return 328 | } 329 | 330 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 331 | } 332 | 333 | // Update implements resource.Resource 334 | func (r *TCPEchoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 335 | var data *TCPEchoResourceModel 336 | 337 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 338 | if resp.Diagnostics.HasError() { 339 | return 340 | } 341 | 342 | r.TCPEcho(ctx, data, &resp.Diagnostics) 343 | if resp.Diagnostics.HasError() { 344 | return 345 | } 346 | 347 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 348 | } 349 | 350 | func NewTCPEchoResource() resource.Resource { 351 | return &TCPEchoResource{} 352 | } 353 | -------------------------------------------------------------------------------- /pkg/provider/resource_local_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package provider 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "time" 25 | 26 | "github.com/google/uuid" 27 | "github.com/hashicorp/terraform-plugin-framework/diag" 28 | tfpath "github.com/hashicorp/terraform-plugin-framework/path" 29 | "github.com/hashicorp/terraform-plugin-framework/resource" 30 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 31 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 32 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 33 | "github.com/hashicorp/terraform-plugin-framework/types" 34 | "github.com/hashicorp/terraform-plugin-log/tflog" 35 | 36 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/helpers" 37 | "github.com/tetratelabs/terraform-provider-checkmate/pkg/modifiers" 38 | ) 39 | 40 | var _ resource.Resource = &LocalCommandResource{} 41 | var _ resource.ResourceWithImportState = &LocalCommandResource{} 42 | 43 | type LocalCommandResource struct{} 44 | 45 | // Schema implements resource.Resource 46 | func (*LocalCommandResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 47 | resp.Schema = schema.Schema{ 48 | MarkdownDescription: "Local Command", 49 | 50 | Attributes: map[string]schema.Attribute{ 51 | "command": schema.StringAttribute{ 52 | MarkdownDescription: "The command to run (passed to `sh -c`)", 53 | Required: true, 54 | }, 55 | "timeout": schema.Int64Attribute{ 56 | MarkdownDescription: "Overall timeout in milliseconds for the check before giving up, default 10000", 57 | Optional: true, 58 | Computed: true, 59 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(10000)}, 60 | }, 61 | "command_timeout": schema.Int64Attribute{ 62 | MarkdownDescription: "Timeout for an individual attempt. If exceeded, the attempt will be considered failure and potentially retried. Default 5000ms", 63 | Optional: true, 64 | Computed: true, 65 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(5000)}, 66 | }, 67 | "interval": schema.Int64Attribute{ 68 | MarkdownDescription: "Interval in milliseconds between attemps. Default 200", 69 | Optional: true, 70 | Computed: true, 71 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(200)}, 72 | }, 73 | "consecutive_successes": schema.Int64Attribute{ 74 | MarkdownDescription: "Number of consecutive successes required before the check is considered successful overall. Defaults to 1.", 75 | Optional: true, 76 | Computed: true, 77 | PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(1)}, 78 | }, 79 | "working_directory": schema.StringAttribute{ 80 | MarkdownDescription: "Working directory where the command will be run. Defaults to the current working directory", 81 | Optional: true, 82 | Computed: true, 83 | PlanModifiers: []planmodifier.String{modifiers.DefaultString(".")}, 84 | }, 85 | "env": schema.MapAttribute{ 86 | ElementType: types.StringType, 87 | MarkdownDescription: "Map of environment variables to apply to the command. Inherits the parent environment", 88 | Optional: true, 89 | }, 90 | "create_file": schema.SingleNestedAttribute{ 91 | MarkdownDescription: "Ensure a file exists with the following contents. The path to this file will be available in the env var CHECKMATE_FILEPATH", 92 | Optional: true, 93 | Attributes: map[string]schema.Attribute{ 94 | "contents": schema.StringAttribute{ 95 | MarkdownDescription: "Contents of the file to create", 96 | Required: true, 97 | }, 98 | "name": schema.StringAttribute{ 99 | MarkdownDescription: "Name of the created file.", 100 | Required: true, 101 | }, 102 | "path": schema.StringAttribute{ 103 | MarkdownDescription: "Path to the file that was created", 104 | Computed: true, 105 | }, 106 | "use_working_dir": schema.BoolAttribute{ 107 | MarkdownDescription: "If true, will use the working directory instead of a temporary directory. Defaults to false.", 108 | Optional: true, 109 | }, 110 | "create_directory": schema.BoolAttribute{ 111 | MarkdownDescription: "Create the target directory if it doesn't exist. Defaults to false.", 112 | Optional: true, 113 | }, 114 | }, 115 | }, 116 | "stdout": schema.StringAttribute{ 117 | MarkdownDescription: "Standard output of the command", 118 | Computed: true, 119 | }, 120 | "stderr": schema.StringAttribute{ 121 | MarkdownDescription: "Standard error output of the command", 122 | Computed: true, 123 | }, 124 | "passed": schema.BoolAttribute{ 125 | Computed: true, 126 | MarkdownDescription: "True if the check passed", 127 | }, 128 | "create_anyway_on_check_failure": schema.BoolAttribute{ 129 | Optional: true, 130 | MarkdownDescription: "If false, the resource will fail to create if the check does not pass. If true, the resource will be created anyway. Defaults to false.", 131 | }, 132 | "id": schema.StringAttribute{ 133 | Computed: true, 134 | MarkdownDescription: "Identifier", 135 | PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, 136 | }, 137 | "keepers": schema.MapAttribute{ 138 | ElementType: types.StringType, 139 | MarkdownDescription: "Arbitrary map of string values that when changed will cause the check to run again.", 140 | Optional: true, 141 | }, 142 | }} 143 | } 144 | 145 | type CreateFileModel struct { 146 | Contents types.String `tfsdk:"contents"` 147 | Path types.String `tfsdk:"path"` 148 | UseWorkingDirectory types.Bool `tfsdk:"use_working_dir"` 149 | Name types.String `tfsdk:"name"` 150 | CreateDirectory types.Bool `tfsdk:"create_directory"` 151 | } 152 | 153 | type LocalCommandResourceModel struct { 154 | Id types.String `tfsdk:"id"` 155 | Command types.String `tfsdk:"command"` 156 | Timeout types.Int64 `tfsdk:"timeout"` 157 | CommandTimeout types.Int64 `tfsdk:"command_timeout"` 158 | Interval types.Int64 `tfsdk:"interval"` 159 | ConsecutiveSuccesses types.Int64 `tfsdk:"consecutive_successes"` 160 | WorkDir types.String `tfsdk:"working_directory"` 161 | Stdout types.String `tfsdk:"stdout"` 162 | Stderr types.String `tfsdk:"stderr"` 163 | Env types.Map `tfsdk:"env"` 164 | CreateFile *CreateFileModel `tfsdk:"create_file"` 165 | IgnoreFailure types.Bool `tfsdk:"create_anyway_on_check_failure"` 166 | Passed types.Bool `tfsdk:"passed"` 167 | Keepers types.Map `tfsdk:"keepers"` 168 | } 169 | 170 | // ImportState implements resource.ResourceWithImportState 171 | func (*LocalCommandResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 172 | resource.ImportStatePassthroughID(ctx, tfpath.Root("id"), req, resp) 173 | } 174 | 175 | // Create implements resource.Resource 176 | func (r *LocalCommandResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 177 | var data LocalCommandResourceModel 178 | 179 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 180 | if resp.Diagnostics.HasError() { 181 | return 182 | } 183 | 184 | data.Id = types.StringValue(uuid.NewString()) 185 | 186 | r.EnsureFile(ctx, &data, &resp.Diagnostics) 187 | if resp.Diagnostics.HasError() { 188 | return 189 | } 190 | 191 | r.RunCommand(ctx, &data, &resp.Diagnostics) 192 | if resp.Diagnostics.HasError() { 193 | return 194 | } 195 | 196 | resp.Diagnostics.Append(resp.State.Set(ctx, data)...) 197 | } 198 | 199 | func (r *LocalCommandResource) RunCommand(ctx context.Context, data *LocalCommandResourceModel, diag *diag.Diagnostics) { 200 | data.Passed = types.BoolValue(false) 201 | data.Stdout = types.StringNull() 202 | data.Stderr = types.StringNull() 203 | 204 | window := helpers.RetryWindow{ 205 | Context: ctx, 206 | Timeout: time.Duration(data.Timeout.ValueInt64()) * time.Millisecond, 207 | Interval: time.Duration(data.Interval.ValueInt64()) * time.Millisecond, 208 | ConsecutiveSuccesses: int(data.ConsecutiveSuccesses.ValueInt64()), 209 | } 210 | 211 | envMap := make(map[string]string) 212 | if !data.Env.IsNull() { 213 | diag.Append(data.Env.ElementsAs(ctx, &envMap, false)...) 214 | if diag.HasError() { 215 | return 216 | } 217 | 218 | } 219 | 220 | if data.CreateFile != nil { 221 | abs, err := filepath.Abs(data.CreateFile.Path.ValueString()) 222 | if err != nil { 223 | tflog.Error(ctx, fmt.Sprintf("Can't determine the absolute path of the file we created at %q error=%v", data.CreateFile.Path.ValueString(), err)) 224 | diag.AddError("Can't get path to file", fmt.Sprintf("Can't determine the absolute path of the file we created at %q error=%v", data.CreateFile.Path.ValueString(), err)) 225 | return 226 | } 227 | envMap["CHECKMATE_FILEPATH"] = abs 228 | } 229 | 230 | env := make([]string, 0, len(envMap)) 231 | for k, v := range envMap { 232 | env = append(env, fmt.Sprintf("%s=%s", k, v)) 233 | } 234 | 235 | tflog.Debug(ctx, fmt.Sprintf("Command string: sh -c %s", data.Command.ValueString())) 236 | result := window.Do(func(attempt int, successes int) bool { 237 | var stdout bytes.Buffer 238 | var stderr bytes.Buffer 239 | 240 | commandContext, cancelFunc := context.WithTimeout(ctx, time.Duration(data.CommandTimeout.ValueInt64())*time.Millisecond) 241 | defer cancelFunc() 242 | 243 | cmd := exec.CommandContext(commandContext, "sh", "-c", data.Command.ValueString()) 244 | cmd.Dir = data.WorkDir.ValueString() 245 | cmd.Stdout = &stdout 246 | cmd.Stderr = &stderr 247 | cmd.Env = append(os.Environ(), env...) 248 | 249 | err := cmd.Start() 250 | if err != nil { 251 | tflog.Trace(ctx, fmt.Sprintf("ATTEMPT #%d error starting command", attempt)) 252 | tflog.Error(ctx, fmt.Sprintf("Error starting command %v", err)) 253 | return false 254 | } 255 | err = cmd.Wait() 256 | if err != nil { 257 | tflog.Warn(ctx, fmt.Sprintf("ATTEMPT #%d exit_code=%d err=%s", attempt, err.(*exec.ExitError).ExitCode(), err.Error())) 258 | data.Stdout = types.StringValue(stdout.String()) 259 | data.Stderr = types.StringValue(stderr.String()) 260 | tflog.Warn(ctx, fmt.Sprintf("Command string: sh -c %s", data.Command.ValueString())) 261 | tflog.Warn(ctx, fmt.Sprintf("Command stdout: %s", stdout.String())) 262 | tflog.Warn(ctx, fmt.Sprintf("Command stderr: %s", stderr.String())) 263 | return false 264 | } 265 | tflog.Trace(ctx, fmt.Sprintf("SUCCESS [%d/%d]", successes, data.ConsecutiveSuccesses.ValueInt64())) 266 | data.Stdout = types.StringValue(stdout.String()) 267 | data.Stderr = types.StringValue(stderr.String()) 268 | tflog.Debug(ctx, fmt.Sprintf("Command stdout: %s", stdout.String())) 269 | tflog.Debug(ctx, fmt.Sprintf("Command stdout: %s", stderr.String())) 270 | return true 271 | }) 272 | 273 | switch result { 274 | case helpers.Success: 275 | data.Passed = types.BoolValue(true) 276 | case helpers.TimeoutExceeded: 277 | diag.AddWarning("Timeout exceeded", fmt.Sprintf("Timeout of %d milliseconds exceeded", data.Timeout.ValueInt64())) 278 | if !data.IgnoreFailure.ValueBool() { 279 | diag.AddError("Check failed", "The check did not pass and create_anyway_on_check_failure is false") 280 | return 281 | } 282 | } 283 | 284 | } 285 | 286 | // Delete implements resource.Resource 287 | func (*LocalCommandResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 288 | var data *LocalCommandResourceModel 289 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 290 | 291 | if resp.Diagnostics.HasError() { 292 | return 293 | } 294 | } 295 | 296 | // Metadata implements resource.Resource 297 | func (*LocalCommandResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 298 | resp.TypeName = req.ProviderTypeName + "_local_command" 299 | } 300 | 301 | // Read implements resource.Resource 302 | func (*LocalCommandResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 303 | var data *LocalCommandResourceModel 304 | 305 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 306 | if resp.Diagnostics.HasError() { 307 | return 308 | } 309 | 310 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 311 | } 312 | 313 | // Update implements resource.Resource 314 | func (r *LocalCommandResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 315 | var data *LocalCommandResourceModel 316 | 317 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 318 | if resp.Diagnostics.HasError() { 319 | return 320 | } 321 | 322 | r.EnsureFile(ctx, data, &resp.Diagnostics) 323 | if resp.Diagnostics.HasError() { 324 | return 325 | } 326 | 327 | r.RunCommand(ctx, data, &resp.Diagnostics) 328 | if resp.Diagnostics.HasError() { 329 | return 330 | } 331 | 332 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 333 | } 334 | 335 | func NewLocalCommandResource() resource.Resource { 336 | return &LocalCommandResource{} 337 | } 338 | 339 | func (r *LocalCommandResource) EnsureFile(ctx context.Context, data *LocalCommandResourceModel, diag *diag.Diagnostics) { 340 | cf := data.CreateFile 341 | if cf == nil { 342 | return 343 | } 344 | 345 | var file *os.File 346 | var err error 347 | if cf.UseWorkingDirectory.ValueBool() { 348 | if cf.CreateDirectory.ValueBool() { 349 | dir := filepath.Join(data.WorkDir.ValueString(), filepath.Dir(cf.Name.ValueString())) 350 | err = os.MkdirAll(dir, 0750) 351 | if err != nil { 352 | diag.AddError("Error creating directory", fmt.Sprintf("Error creating directory %q. %v", dir, err)) 353 | return 354 | } 355 | 356 | } 357 | file, err = os.CreateTemp(data.WorkDir.ValueString(), cf.Name.ValueString()) 358 | if err != nil { 359 | diag.AddError("Error creating file", fmt.Sprintf("%s", err)) 360 | return 361 | } 362 | } else { 363 | file, err = os.CreateTemp("", cf.Name.ValueString()) 364 | if err != nil { 365 | diag.AddError("Error creating file", fmt.Sprintf("%s", err)) 366 | return 367 | } 368 | } 369 | 370 | _, err = file.WriteString(cf.Contents.ValueString()) 371 | if err != nil { 372 | diag.AddError("Error writing file", fmt.Sprintf("%s", err)) 373 | return 374 | } 375 | 376 | cf.Path = types.StringValue(file.Name()) 377 | } 378 | -------------------------------------------------------------------------------- /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/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 6 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 7 | github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= 8 | github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= 9 | github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= 10 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 11 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 12 | github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= 13 | github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 14 | github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= 15 | github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 16 | github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= 17 | github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 18 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= 19 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 20 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 21 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 22 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 23 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 24 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= 25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 26 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 27 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 28 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 29 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 30 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 31 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 32 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 33 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 38 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 39 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 40 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 41 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 42 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 43 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 44 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 45 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 46 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 47 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 48 | github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= 49 | github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= 50 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 51 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 52 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 53 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 54 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 55 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 56 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 57 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 58 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 59 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 60 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 61 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 62 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 63 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 64 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 65 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 66 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 67 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 68 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 69 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 70 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= 71 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 72 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 73 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 74 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 75 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= 76 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 77 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 78 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 79 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 80 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 81 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 82 | github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= 83 | github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= 84 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 85 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 86 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 87 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 88 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 89 | github.com/hashicorp/hc-install v0.6.1 h1:IGxShH7AVhPaSuSJpKtVi/EFORNjO+OYVJJrAtGG2mY= 90 | github.com/hashicorp/hc-install v0.6.1/go.mod h1:0fW3jpg+wraYSnFDJ6Rlie3RvLf1bIqVIkzoon4KoVE= 91 | github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= 92 | github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= 93 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 94 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 95 | github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= 96 | github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= 97 | github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA= 98 | github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= 99 | github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI= 100 | github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA= 101 | github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI= 102 | github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY= 103 | github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= 104 | github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= 105 | github.com/hashicorp/terraform-plugin-go v0.19.1 h1:lf/jTGTeELcz5IIbn/94mJdmnTjRYm6S6ct/JqCSr50= 106 | github.com/hashicorp/terraform-plugin-go v0.19.1/go.mod h1:5NMIS+DXkfacX6o5HCpswda5yjkSYfKzn1Nfl9l+qRs= 107 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 108 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 109 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 h1:X7vB6vn5tON2b49ILa4W7mFAsndeqJ7bZFOGbVO+0Cc= 110 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0/go.mod h1:ydFcxbdj6klCqYEPkPvdvFKiNGKZLUs+896ODUXCyao= 111 | github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= 112 | github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= 113 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= 114 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= 115 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= 116 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 117 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 118 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 119 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 120 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 121 | github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= 122 | github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 123 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 124 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 125 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 126 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 127 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 128 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 129 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 130 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 131 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 132 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 133 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 134 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 135 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 136 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 137 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 138 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 139 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 140 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 141 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 142 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 143 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 144 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 145 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 146 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 147 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 148 | github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= 149 | github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= 150 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 151 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 152 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 153 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 154 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 155 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 156 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 157 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 158 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 159 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 160 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 161 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 162 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 163 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 164 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 165 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 166 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 167 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 168 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 169 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= 170 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 171 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 172 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 173 | github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= 174 | github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= 175 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 176 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 177 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 178 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 179 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 180 | github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= 181 | github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= 182 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 183 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 184 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 185 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 186 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 187 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 188 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 189 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 190 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 191 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 192 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 193 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 194 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 195 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 196 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 197 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 198 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 199 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 200 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 201 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 202 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 203 | github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= 204 | github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 205 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 206 | golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 207 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 208 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 209 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 210 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 211 | golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= 212 | golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= 213 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= 214 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 215 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 216 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 217 | golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= 218 | golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 219 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 220 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 221 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 222 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 223 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 224 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 225 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 226 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 227 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 228 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 229 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 234 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 239 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 240 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 250 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 251 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 252 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 253 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 254 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 255 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 256 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 257 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 258 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 259 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 260 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 261 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 262 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 263 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 264 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 265 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 266 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 267 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 268 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 269 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 270 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 271 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 272 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 273 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 274 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 275 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 276 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 277 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 278 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 279 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 280 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 281 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 282 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 283 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 284 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 285 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 286 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 287 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 288 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 289 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 290 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 291 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 292 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 293 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 294 | k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= 295 | k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= 296 | --------------------------------------------------------------------------------