diff --git a/.copywrite.hcl b/.copywrite.hcl new file mode 100644 index 0000000..931d34f --- /dev/null +++ b/.copywrite.hcl @@ -0,0 +1,21 @@ +# NOTE: This file is for HashiCorp specific licensing automation and can be deleted after creating a new repo with this template. +schema_version = 1 + +project { + license = "MPL-2.0" + copyright_year = 2024 + + header_ignore = [ + # examples used within documentation (prose) + "examples/**", + + # GitHub issue template configuration + ".github/ISSUE_TEMPLATE/*.yml", + + # golangci-lint tooling configuration + ".golangci.yml", + + # GoReleaser tooling configuration + ".goreleaser.yml", + ] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..922ee27 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @hashicorp/terraform-devex diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0c8b092 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. + +Please read the full text at https://www.hashicorp.com/community-guidelines diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..73bb4d3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# See GitHub's documentation for more information on this file: +# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + # TODO: Dependabot only updates hashicorp GHAs in the template repository, the following lines can be removed for consumers of this template + allow: + - dependency-name: "hashicorp/*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3eb4d1f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +# Terraform Provider release workflow. +name: Release + +# This GitHub action creates a release when a tag that matches the pattern +# "v*" (e.g. v0.1.0) is created. +on: + push: + tags: + - 'v*' + +# Releases need permissions to read and write the repository contents. +# GitHub considers creating releases and uploading assets as writing contents. +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + # Allow goreleaser to access older tag information. + fetch-depth: 0 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: 'go.mod' + cache: true + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + id: import_gpg + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 + with: + args: release --clean + env: + # GitHub sets the GITHUB_TOKEN secret automatically. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7827c97 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,85 @@ +# Terraform Provider testing workflow. +name: Tests + +# This GitHub action runs your tests for each pull request and push. +# Optionally, you can turn it on using a schedule for regular testing. +on: + pull_request: + paths-ignore: + - 'README.md' + push: + paths-ignore: + - 'README.md' + +# Testing only needs permissions to read the repository contents. +permissions: + contents: read + +jobs: + # Ensure project builds before running testing matrix + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - run: go build -v . + - name: Run linters + uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0 + with: + version: latest + + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 + with: + terraform_version: '1.9.4' + terraform_wrapper: false + - run: go generate ./... + - name: git diff + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) + + # Run acceptance tests in a matrix with Terraform CLI versions + test: + name: Terraform Provider Acceptance Tests + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + # list whatever Terraform versions here you would like to support + terraform: + - '1.5.*' + - '1.6.*' + - '1.7.*' + - '1.8.*' + - '1.9.*' + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 + with: + terraform_version: ${{ matrix.terraform }} + terraform_wrapper: false + - run: go mod download + - env: + TF_ACC: "1" + run: go test -v -cover ./internal/provider/ + timeout-minutes: 10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eac76d --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +*.dll +*.exe +.DS_Store +example.tf +terraform.tfplan +terraform.tfstate +bin/ +dist/ +modules-dev/ +/pkg/ +website/.vagrant +website/.bundle +website/build +website/node_modules +.vagrant/ +*.backup +./*.tfstate +.terraform/ +*.log +*.bak +*~ +.*.swp +.idea +*.iml +*.test +*.iml + +website/vendor + +# Test exclusions +!command/test-fixtures/**/*.tfstate +!command/test-fixtures/**/.terraform/ + +# Keep windows files with windows line endings +*.winfile eol=crlf +examples/provider-install-verification diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4877406 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,27 @@ +# Visit https://golangci-lint.run/ for usage documentation +# and information on other useful linters +issues: + max-per-linter: 0 + max-same-issues: 0 + +linters: + disable-all: true + enable: + - durationcheck + - errcheck + - exportloopref + - forcetypeassert + - godot + - gofmt + - gosimple + - ineffassign + - makezero + - misspell + - nilerr + - predeclared + - staticcheck + - tenv + - unconvert + - unparam + - unused + - govet \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..ecbe524 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,61 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# See https://goreleaser.com/customization/ for more information +version: 2 +before: + hooks: + - go generate ./... + - go mod tidy +builds: + - env: + # goreleaser does not work with CGO, it could also complicate + # usage by users in CI/CD systems like Terraform Cloud where + # they are unable to install libraries. + - CGO_ENABLED=0 + mod_timestamp: "{{ .CommitTimestamp }}" + flags: + - -trimpath + ldflags: + - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}" + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm + - arm64 + ignore: + - goos: darwin + goarch: '386' + binary: "{{ .ProjectName }}_v{{ .Version }}" +archives: + - format: zip + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" +checksum: + extra_files: + - glob: "terraform-registry-manifest.json" + name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" + name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" + algorithm: sha256 +signs: + - artifacts: checksum + args: + # if you are using this in a GitHub action or some other automated pipeline, you + # need to pass the batch flag to indicate its not interactive. + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" +release: + extra_files: + - glob: "terraform-registry-manifest.json" + name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" + # If you want to manually examine the release before its live, uncomment this line: + # draft: true +changelog: + disable: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b76e247 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 (Unreleased) + +FEATURES: diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..7771cd6 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,6 @@ +default: testacc + +# Run acceptance tests +.PHONY: testacc +testacc: + TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m diff --git a/docs/data-sources/servicemanager_dart_versions.md b/docs/data-sources/servicemanager_dart_versions.md new file mode 100644 index 0000000..9460680 --- /dev/null +++ b/docs/data-sources/servicemanager_dart_versions.md @@ -0,0 +1,5 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: " - " +subcategory: "" +description: |- diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..026c42c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Examples + +This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. + +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. + +* **provider/provider.tf** example file for the provider index page +* **data-sources/`full data source name`/data-source.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named data source page diff --git a/examples/data-sources/turso_database/resource.tf b/examples/data-sources/turso_database/resource.tf new file mode 100644 index 0000000..87e40c7 --- /dev/null +++ b/examples/data-sources/turso_database/resource.tf @@ -0,0 +1,3 @@ +data "turso_database" "example" { + name = "a-database" +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf new file mode 100644 index 0000000..1e13dee --- /dev/null +++ b/examples/provider/provider.tf @@ -0,0 +1,3 @@ +provider "turso" { + api_token = "" +} diff --git a/examples/resources/turso_database/import.sh b/examples/resources/turso_database/import.sh new file mode 100644 index 0000000..93ab67b --- /dev/null +++ b/examples/resources/turso_database/import.sh @@ -0,0 +1 @@ +terraform import turso_database.example_database database_name \ No newline at end of file diff --git a/examples/resources/turso_database/resource.tf b/examples/resources/turso_database/resource.tf new file mode 100644 index 0000000..9d80e29 --- /dev/null +++ b/examples/resources/turso_database/resource.tf @@ -0,0 +1,4 @@ +resource "turso_database" "example" { + group = "a-group" + name = "a-database" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4271794 --- /dev/null +++ b/go.mod @@ -0,0 +1,103 @@ +module github.com/celest-dev/terraform-provider-utils + +go 1.22.4 + +require ( + cloud.google.com/go/longrunning v0.5.12 + cloud.google.com/go/servicemanagement v1.9.9 + github.com/coreos/go-semver v0.3.1 + github.com/hashicorp/terraform-plugin-framework v1.10.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 + golang.org/x/oauth2 v0.22.0 + golang.org/x/sync v0.8.0 + google.golang.org/api v0.191.0 + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 +) + +replace github.com/celest-dev/cloud/packages/tursoadmin-go v0.0.0 => ../../../packages/tursoadmin-go + +require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/auth v0.8.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/iam v1.1.12 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/hashicorp/cli v1.1.6 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hc-install v0.7.0 // indirect + github.com/hashicorp/terraform-exec v0.21.0 // indirect + github.com/hashicorp/terraform-json v0.22.1 // indirect + github.com/hashicorp/terraform-plugin-docs v0.19.4 // indirect + github.com/hashicorp/terraform-plugin-go v0.23.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.3 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/yuin/goldmark v1.7.1 // indirect + github.com/yuin/goldmark-meta v1.1.0 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.6.0 // indirect + google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43afaaa --- /dev/null +++ b/go.sum @@ -0,0 +1,332 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/auth v0.8.0 h1:y8jUJLl/Fg+qNBWxP/Hox2ezJvjkrPb952PC1p0G6A4= +cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc= +cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= +cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw= +cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg= +cloud.google.com/go/longrunning v0.5.12 h1:5LqSIdERr71CqfUsFlJdBpOkBH8FBCFD7P1nTWy3TYE= +cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU= +cloud.google.com/go/servicemanagement v1.9.9 h1:4O7bR5YZ8cyeT6GcV0ay19Dxek32x+92tJVQNjVSP7c= +cloud.google.com/go/servicemanagement v1.9.9/go.mod h1:C4ceppUpmjJKFipP36T4yN2RMTED3muAtmwXAaEI0ug= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= +github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8= +github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk= +github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= +github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= +github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= +github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= +github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c= +github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA= +github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= +github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= +github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E= +github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo= +github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= +github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= +github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= +github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= +go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.191.0 h1:cJcF09Z+4HAB2t5qTQM1ZtfL/PemsLFkcFG67qq2afk= +google.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf h1:OqdXDEakZCVtDiZTjcxfwbHPCT11ycCEsTKesBVKvyY= +google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/provider/data_dart_versions.go b/internal/provider/data_dart_versions.go new file mode 100644 index 0000000..79d761a --- /dev/null +++ b/internal/provider/data_dart_versions.go @@ -0,0 +1,219 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "regexp" + "sort" + "strings" + + "github.com/coreos/go-semver/semver" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "golang.org/x/sync/errgroup" +) + +type DartVersionsDataSource struct{} + +type DartVersionsDataSourceModel struct { + SdkType types.String `tfsdk:"sdk_type"` + MinVersion types.String `tfsdk:"min_version"` + IncludePrerelease types.Bool `tfsdk:"include_prerelease"` + + // Computed + ID types.String `tfsdk:"id"` + Versions types.List `tfsdk:"versions"` + ContainerVersions types.List `tfsdk:"container_versions"` +} + +func NewDartVersionsDataSource() datasource.DataSource { + return &DartVersionsDataSource{} +} + +// Metadata implements datasource.DataSource. +func (s *DartVersionsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dart_versions" +} + +// Schema implements datasource.DataSource. +func (s *DartVersionsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A list of Dart SDK versions.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the config. Format: `{sdkType}/{minVersion}`.", + Computed: true, + }, + "sdk_type": schema.StringAttribute{ + MarkdownDescription: "The type of SDK.", + Required: true, + }, + "min_version": schema.StringAttribute{ + MarkdownDescription: "The minimum version of the SDK.", + Required: true, + }, + "include_prerelease": schema.BoolAttribute{ + MarkdownDescription: "Whether to include pre-release versions.", + Optional: true, + }, + "versions": schema.ListAttribute{ + MarkdownDescription: "The list of versions.", + Computed: true, + ElementType: basetypes.StringType{}, + }, + "container_versions": schema.ListAttribute{ + MarkdownDescription: "The list of container versions. This excludes patch versions except for pre-release versions.", + Computed: true, + ElementType: basetypes.StringType{}, + }, + }, + } +} + +func (d *DartVersionsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +} + +func (d *DartVersionsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + model := DartVersionsDataSourceModel{} + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + channels := []string{"stable", "beta", "dev"} + eg := new(errgroup.Group) + + versionsChan := make(chan []string) + + for _, channel := range channels { + channel := channel + eg.Go(func() error { + versions, err := d.listVersions(channel) + if err != nil { + return err + } + versionsChan <- versions + return nil + }) + } + + go func() { + err := eg.Wait() + if err != nil { + resp.Diagnostics.AddError("Failed to list versions", err.Error()) + } + close(versionsChan) + }() + + versionsSet := make(map[string]struct{}) + for versions := range versionsChan { + if versions == nil { + continue + } + for _, version := range versions { + versionsSet[version] = struct{}{} + } + } + + minVersion := semver.New(model.MinVersion.ValueString()) + includePrerelease := model.IncludePrerelease.ValueBool() + model.IncludePrerelease = types.BoolValue(includePrerelease) + + versions := make([]*semver.Version, 0, len(versionsSet)) + for version := range versionsSet { + semversion := semver.New(version) + if semversion.LessThan(*minVersion) { + continue + } + if semversion.PreRelease != "" && !includePrerelease { + continue + } + versions = append(versions, semversion) + } + semver.Sort(versions) + + versionAttrs := make([]attr.Value, 0, len(versions)) + for _, version := range versions { + versionAttrs = append(versionAttrs, types.StringValue(version.String())) + } + + containerVersionSet := map[string]struct{}{} + for _, version := range versions { + if version.PreRelease != "" { + containerVersionSet[version.String()] = struct{}{} + continue + } + minorVersion := fmt.Sprintf("%d.%d", version.Major, version.Minor) + containerVersionSet[minorVersion] = struct{}{} + } + + containerVersions := make([]string, 0, len(containerVersionSet)) + for version := range containerVersionSet { + containerVersions = append(containerVersions, version) + } + sort.Strings(containerVersions) + + containerVersionAttrs := make([]attr.Value, 0, len(containerVersions)) + for _, version := range containerVersions { + containerVersionAttrs = append(containerVersionAttrs, types.StringValue(version)) + } + + model.ID = types.StringValue( + fmt.Sprintf("%s/%s", model.SdkType.ValueString(), model.MinVersion.ValueString()), + ) + versionsAttr, diags := types.ListValue(basetypes.StringType{}, versionAttrs) + resp.Diagnostics.Append(diags...) + model.Versions = versionsAttr + + containerVersionsAttr, diags := types.ListValue(basetypes.StringType{}, containerVersionAttrs) + resp.Diagnostics.Append(diags...) + model.ContainerVersions = containerVersionsAttr + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`) + +func (d *DartVersionsDataSource) listVersions(channel string) ([]string, error) { + url, _ := url.Parse("https://www.googleapis.com/storage/v1/b/dart-archive/o") + query := url.Query() + query.Set("prefix", fmt.Sprintf("channels/%s/release/", channel)) + query.Set("delimiter", "/") + url.RawQuery = query.Encode() + + resp, err := http.Get(url.String()) + if err != nil { + return nil, fmt.Errorf("failed to list %s versions: %w", channel, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to list %s versions: %s", channel, resp.Status) + } + + var response struct { + Prefixes []string `json:"prefixes"` + } + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode %s response: %w", channel, err) + } + + versions := make([]string, 0, len(response.Prefixes)) +prefixes: + for _, prefix := range response.Prefixes { + parts := strings.Split(prefix, "/") + for _, part := range parts { + if versionRegex.Match([]byte(part)) { + versions = append(versions, part) + continue prefixes + } + } + } + + return versions, nil +} diff --git a/internal/provider/data_service_configuration.go b/internal/provider/data_service_configuration.go new file mode 100644 index 0000000..ebd755b --- /dev/null +++ b/internal/provider/data_service_configuration.go @@ -0,0 +1,104 @@ +package provider + +import ( + "context" + "fmt" + + servicemanagement "cloud.google.com/go/servicemanagement/apiv1" + "cloud.google.com/go/servicemanagement/apiv1/servicemanagementpb" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "google.golang.org/protobuf/encoding/protojson" +) + +type ServiceConfigDataSource struct { + ServiceManagerClient *servicemanagement.ServiceManagerClient +} + +type ServiceConfigDataSourceModel struct { + ID types.String `tfsdk:"id"` + + // Computed + ServiceConfigJSON types.String `tfsdk:"service_config_json"` +} + +// Metadata implements datasource.DataSource. +func (s *ServiceConfigDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_config" +} + +// Schema implements datasource.DataSource. +func (s *ServiceConfigDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A service manager service configuration.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the config. Format: `{serviceName}/{configId}`.", + Required: true, + }, + "service_config_json": schema.StringAttribute{ + MarkdownDescription: "The service config in JSON format.", + Computed: true, + }, + }, + } +} + +func (d *ServiceConfigDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*UtilsProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *UtilsProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.ServiceManagerClient = config.ServiceManagerClient +} + +// Read implements datasource.DataSource. +func (d *ServiceConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data ServiceConfigDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + serviceName, configID, err := parseConfigId(data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to parse config ID", err.Error()) + return + } + config, err := d.ServiceManagerClient.GetServiceConfig(ctx, &servicemanagementpb.GetServiceConfigRequest{ + ServiceName: serviceName, + ConfigId: configID, + View: servicemanagementpb.GetServiceConfigRequest_FULL, + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get service config", err.Error()) + return + } + + configJSON, err := protojson.Marshal(config) + if err != nil { + resp.Diagnostics.AddError("Failed to marshal service config", err.Error()) + return + } + + data.ServiceConfigJSON = types.StringValue(string(configJSON)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func NewServiceConfigDataSource() datasource.DataSource { + return &ServiceConfigDataSource{} +} + +var _ datasource.DataSource = &ServiceConfigDataSource{} +var _ datasource.DataSourceWithConfigure = &ServiceConfigDataSource{} diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 0000000..0553a82 --- /dev/null +++ b/internal/provider/provider.go @@ -0,0 +1,173 @@ +package provider + +import ( + "context" + + lrauto "cloud.google.com/go/longrunning/autogen" + servicemanagement "cloud.google.com/go/servicemanagement/apiv1" + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "golang.org/x/oauth2" + googleoauth "golang.org/x/oauth2/google" + "google.golang.org/api/option" + "google.golang.org/api/serviceconsumermanagement/v1" + "google.golang.org/grpc/credentials/oauth" +) + +// Ensure UtilsProvider satisfies various provider interfaces. +var _ provider.Provider = &UtilsProvider{} +var _ provider.ProviderWithConfigValidators = &UtilsProvider{} + +// scopes are the required OAuth scopes for the provider. +var scopes = []string{ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/service.management", +} + +// UtilsProvider defines the provider implementation. +type UtilsProvider struct { + // version is set to the provider version on release, "dev" when the + // provider is built and ran locally, and "test" when running acceptance + // testing. + version string +} + +// UtilsProviderConfig holds the necessary GCP configuration for the provider. +type UtilsProviderConfig struct { + // ServiceManagerClient is the authenticated client for `servicemanagement.googleapis.com`. + ServiceManagerClient *servicemanagement.ServiceManagerClient + + // TenantClient is the authenticated client for `serviceconsumermanagement.googleapis.com`. + TenantClient *serviceconsumermanagement.APIService + + // OperationsClient is the authenticated operations client for `servicemanagement.googleapis.com`. + OperationsClient *lrauto.OperationsClient +} + +// UtilsProviderModel describes the provider data model. +type UtilsProviderModel struct { + // ProjectID is the GCP project to use for requests. + ProjectID types.String `tfsdk:"project_id"` + + // Optional. AccessToken is the optional GCP access token. + AccessToken types.String `tfsdk:"access_token"` +} + +func (p *UtilsProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "utils" + resp.Version = p.version +} + +func (p *UtilsProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + MarkdownDescription: "GCP project ID", + Required: true, + }, + "access_token": schema.StringAttribute{ + MarkdownDescription: "Optional. GCP access token", + Optional: true, + }, + }, + } +} + +func (p *UtilsProvider) ConfigValidators(ctx context.Context) []provider.ConfigValidator { + return []provider.ConfigValidator{ + providervalidator.Conflicting(path.MatchRoot("credentials"), path.MatchRoot("access_token")), + } +} + +func (p *UtilsProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + var data UtilsProviderModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Resources created here must be alive for the lifetime of the provider. + persistentCtx := context.Background() + + dialOpts := []option.ClientOption{ + option.WithQuotaProject(data.ProjectID.ValueString()), + } + switch { + case !data.AccessToken.IsUnknown() && !data.AccessToken.IsNull(): + tflog.Info(ctx, "Configuring with access token") + dialOpts = append(dialOpts, option.WithTokenSource(&oauth.TokenSource{ + TokenSource: oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: data.AccessToken.ValueString(), + }), + })) + + default: + tflog.Info(ctx, "Configuring with default credentials") + + creds, err := googleoauth.FindDefaultCredentialsWithParams(persistentCtx, googleoauth.CredentialsParams{ + Scopes: scopes, + }) + if err != nil { + resp.Diagnostics.AddError("Could not get default credentials", err.Error()) + return + } + dialOpts = append(dialOpts, option.WithCredentials(creds)) + } + + client, err := servicemanagement.NewServiceManagerClient(persistentCtx, dialOpts...) + if err != nil { + resp.Diagnostics.AddError("Could not create service manager client", err.Error()) + return + } + tenantClient, err := serviceconsumermanagement.NewService(persistentCtx, dialOpts...) + if err != nil { + resp.Diagnostics.AddError("Could not create tenant client", err.Error()) + return + } + operations, err := lrauto.NewOperationsClient(persistentCtx, dialOpts...) + if err != nil { + resp.Diagnostics.AddError("Could not create operations client", err.Error()) + return + } + + config := &UtilsProviderConfig{ + ServiceManagerClient: client, + TenantClient: tenantClient, + OperationsClient: operations, + } + resp.ResourceData = config + resp.DataSourceData = config +} + +func (p *UtilsProvider) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + NewServiceResource, + NewServiceConfigResource, + NewServiceRolloutResource, + NewServiceProjectResource, + NewServiceTenancyUnitResource, + } +} + +func (p *UtilsProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + NewDartVersionsDataSource, + NewServiceConfigDataSource, + } +} + +func New(version string) func() provider.Provider { + return func() provider.Provider { + return &UtilsProvider{ + version: version, + } + } +} diff --git a/internal/provider/resource_service.go b/internal/provider/resource_service.go new file mode 100644 index 0000000..787c3c2 --- /dev/null +++ b/internal/provider/resource_service.go @@ -0,0 +1,200 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "cloud.google.com/go/servicemanagement/apiv1/servicemanagementpb" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ServiceResource{} +var _ resource.ResourceWithImportState = &ServiceResource{} + +func NewServiceResource() resource.Resource { + return &ServiceResource{} +} + +// ServiceResource defines the resource implementation. +type ServiceResource struct { + UtilsProviderConfig +} + +// ServiceResource Model describes the resource data model. +type ServiceResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` + ProducerProjectId types.String `tfsdk:"producer_project_id"` + DefaultTenancyUnit types.String `tfsdk:"default_tenancy_unit"` +} + +func (r *ServiceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service" +} + +func (r *ServiceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "A service manager service.", + + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + MarkdownDescription: "The name of the service.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "producer_project_id": schema.StringAttribute{ + MarkdownDescription: "The producer project id.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "default_tenancy_unit": schema.StringAttribute{ + MarkdownDescription: "The tenancy unit assigned to the producer project which holds consumer projects/resources not yet assigned to Celest users.", + Computed: true, + }, + }, + } +} + +func (r *ServiceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + clients, ok := req.ProviderData.(*UtilsProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *UtilsProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.ServiceManagerClient = clients.ServiceManagerClient + r.OperationsClient = clients.OperationsClient +} + +func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + _, err := r.ServiceManagerClient.GetService(ctx, &servicemanagementpb.GetServiceRequest{ + ServiceName: data.ServiceName.ValueString(), + }) + + if err == nil { + resp.Diagnostics.AddError("Service already exists", fmt.Sprintf("Service %s already exists", data.ServiceName.ValueString())) + return + } else if status.Code(err) != codes.NotFound && !strings.Contains(err.Error(), "not found") { + resp.Diagnostics.AddError("Error getting service", err.Error()) + return + } + + serviceOp, err := r.ServiceManagerClient.CreateService(ctx, &servicemanagementpb.CreateServiceRequest{ + Service: &servicemanagementpb.ManagedService{ + ServiceName: data.ServiceName.ValueString(), + ProducerProjectId: data.ProducerProjectId.ValueString(), + }, + }) + + if err != nil { + resp.Diagnostics.AddError("Error creating service", err.Error()) + return + } + + service, err := serviceOp.Wait(ctx) + if err != nil { + resp.Diagnostics.AddError("Error creating service", err.Error()) + return + } + + data.ServiceName = types.StringValue(service.ServiceName) + data.ProducerProjectId = types.StringValue(service.ProducerProjectId) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + service, err := r.ServiceManagerClient.GetService(ctx, &servicemanagementpb.GetServiceRequest{ + ServiceName: data.ServiceName.ValueString(), + }) + + if err != nil { + if err, ok := status.FromError(err); ok && (err.Code() == codes.NotFound || strings.Contains(err.String(), "not found")) { + return + } + resp.Diagnostics.AddError("Could not retrieve service", err.Error()) + return + } + + data.ServiceName = types.StringValue(service.ServiceName) + data.ProducerProjectId = types.StringValue(service.ProducerProjectId) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + panic("Updating a service is not supported") +} + +func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data ServiceResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + op, err := r.ServiceManagerClient.DeleteService(ctx, &servicemanagementpb.DeleteServiceRequest{ + ServiceName: data.ServiceName.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError("Error deleting service", err.Error()) + return + } + + if err := op.Wait(ctx); err != nil { + resp.Diagnostics.AddError("Error deleting service", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &ServiceResourceModel{})...) +} + +func (r *ServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("service_name"), req, resp) +} diff --git a/internal/provider/resource_service_configuration.go b/internal/provider/resource_service_configuration.go new file mode 100644 index 0000000..0e53043 --- /dev/null +++ b/internal/provider/resource_service_configuration.go @@ -0,0 +1,240 @@ +package provider + +import ( + "context" + "encoding/base64" + "fmt" + + "cloud.google.com/go/servicemanagement/apiv1/servicemanagementpb" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ServiceConfigResource{} +var _ resource.ResourceWithImportState = &ServiceConfigResource{} + +func NewServiceConfigResource() resource.Resource { + return &ServiceConfigResource{} +} + +// ServiceResource defines the resource implementation. +type ServiceConfigResource struct { + UtilsProviderConfig +} + +type ServiceConfigResourceModel struct { + Id types.String `tfsdk:"id"` + ServiceName types.String `tfsdk:"service_name"` + ConfigYaml types.String `tfsdk:"config_yaml"` + ProtoDescriptorBase64 types.String `tfsdk:"proto_descriptor_base64"` +} + +func (r *ServiceConfigResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_config" +} + +func (r *ServiceConfigResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "A service manager service.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the config.", + Computed: true, + }, + "service_name": schema.StringAttribute{ + MarkdownDescription: "The name of the service.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "config_yaml": schema.StringAttribute{ + MarkdownDescription: "The service config in YAML format.", + Required: true, + }, + "proto_descriptor_base64": schema.StringAttribute{ + MarkdownDescription: "The base64-encoded proto descriptor.", + Required: true, + Sensitive: true, // Not sensitive but suppress from output + }, + }, + } +} + +func (r *ServiceConfigResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*UtilsProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *UtilsProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.ServiceManagerClient = config.ServiceManagerClient + r.OperationsClient = config.OperationsClient +} + +// Create implements resource.Resource. +func (r *ServiceConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceConfigResourceModel + + // This will populate the data struct with the values from the plan. + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + output, err := r.createConfig(ctx, data.ServiceName.ValueString(), data.ProtoDescriptorBase64.ValueString(), data.ConfigYaml.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Could not submit configuration source", err.Error()) + return + } + + data.Id = newConfigId(output.ServiceConfig.GetName(), output.ServiceConfig.GetId()) + + // Save created data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Read implements resource.Resource. +func (r *ServiceConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceConfigResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + serviceName, configId, err := parseConfigId(data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid config ID", err.Error()) + return + } + + tflog.Debug(ctx, "Reading service config", map[string]interface{}{ + "service_name": serviceName, + "config_id": configId, + }) + config, err := r.ServiceManagerClient.GetServiceConfig(ctx, &servicemanagementpb.GetServiceConfigRequest{ + ServiceName: serviceName, + ConfigId: configId, + View: servicemanagementpb.GetServiceConfigRequest_FULL, + }) + + if err != nil { + resp.Diagnostics.AddError("Could not retrieve configuration for service", err.Error()) + return + } + + tflog.Debug(ctx, "Retrieved service config") + + data.Id = newConfigId(config.Name, config.Id) + data.ServiceName = types.StringValue(config.Name) + + sourceFiles := config.GetSourceInfo().GetSourceFiles() + for _, sourceFile := range sourceFiles { + // SourceFiles are of type google.api.servicemanagement.v1.ConfigFile + // https://cloud.google.com/service-infrastructure/docs/service-management/reference/rest/v1/ConfigView + var file servicemanagementpb.ConfigFile + if err := sourceFile.UnmarshalTo(&file); err != nil { + resp.Diagnostics.AddError("Could not unmarshal source file", err.Error()) + return + } + + tflog.Debug(ctx, "Discovered source file", map[string]interface{}{ + "file_path": file.GetFilePath(), + "file_type": file.GetFileType(), + }) + + switch file.FileType { + case servicemanagementpb.ConfigFile_FILE_DESCRIPTOR_SET_PROTO: + data.ProtoDescriptorBase64 = types.StringValue(base64.StdEncoding.EncodeToString(file.GetFileContents())) + case servicemanagementpb.ConfigFile_SERVICE_CONFIG_YAML: + data.ConfigYaml = types.StringValue(string(file.GetFileContents())) + default: + resp.Diagnostics.AddError("Unknown file type", fmt.Sprintf("Unknown file type: %v", file.FileType)) + } + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Update implements resource.Resource. +func (r *ServiceConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data ServiceConfigResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + output, err := r.createConfig(ctx, data.ServiceName.ValueString(), data.ProtoDescriptorBase64.ValueString(), data.ConfigYaml.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Could not submit configuration source", err.Error()) + return + } + + data.Id = newConfigId(output.ServiceConfig.GetName(), output.ServiceConfig.GetId()) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Delete implements resource.Resource. +func (r *ServiceConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Nothing to do +} + +// ImportState implements resource.ResourceWithImportState. +func (r *ServiceConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ServiceConfigResource) createConfig(ctx context.Context, serviceName, protoDescriptor, configYaml string) (*servicemanagementpb.SubmitConfigSourceResponse, error) { + proto, err := base64.StdEncoding.DecodeString(protoDescriptor) + if err != nil { + return nil, fmt.Errorf("could not decode proto descriptor: %w", err) + } + configOp, err := r.ServiceManagerClient.SubmitConfigSource(ctx, &servicemanagementpb.SubmitConfigSourceRequest{ + ServiceName: serviceName, + ConfigSource: &servicemanagementpb.ConfigSource{ + Files: []*servicemanagementpb.ConfigFile{ + { + FileContents: []byte(configYaml), + FilePath: "service.yaml", + FileType: servicemanagementpb.ConfigFile_SERVICE_CONFIG_YAML, + }, + { + FileContents: proto, + FilePath: "descriptor.pb", + FileType: servicemanagementpb.ConfigFile_FILE_DESCRIPTOR_SET_PROTO, + }, + }, + }, + }) + + if err != nil { + return nil, err + } + + config, err := configOp.Wait(ctx) + if err != nil { + return nil, err + } + + return config, nil +} diff --git a/internal/provider/resource_service_project.go b/internal/provider/resource_service_project.go new file mode 100644 index 0000000..55edf49 --- /dev/null +++ b/internal/provider/resource_service_project.go @@ -0,0 +1,544 @@ +package provider + +import ( + "context" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "google.golang.org/api/serviceconsumermanagement/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ServiceProjectResource{} + +func NewServiceProjectResource() resource.Resource { + return &ServiceProjectResource{} +} + +// ServiceProjectResource defines the resource implementation. +type ServiceProjectResource struct { + UtilsProviderConfig +} + +// ServiceProjectResourceModel describes the resource data model. +type ServiceProjectResourceModel struct { + ID types.String `tfsdk:"id"` + TenancyUnit types.String `tfsdk:"tenancy_unit"` + Tag types.String `tfsdk:"tag"` + ProjectConfig types.Object `tfsdk:"project_config"` + + // Computed + Status types.String `tfsdk:"status"` +} + +type ServiceProjectConfigModel struct { + Folder types.String `tfsdk:"folder"` + TenantProjectPolicy types.Object `tfsdk:"tenant_project_policy"` + Labels types.Map `tfsdk:"labels"` + Services types.List `tfsdk:"services"` + BillingConfig types.Object `tfsdk:"billing_config"` + ServiceAccountConfig types.Object `tfsdk:"service_account_config"` +} + +func (ServiceProjectConfigModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "folder": types.StringType, + "tenant_project_policy": types.ObjectType{ + AttrTypes: ServiceProjectConfigTenantProjectPolicyModel{}.AttributeTypes(), + }, + "labels": types.MapType{ElemType: types.StringType}, + "services": types.ListType{ElemType: types.StringType}, + "billing_config": types.ObjectType{ + AttrTypes: ServiceProjectConfigBillingConfigModel{}.AttributeTypes(), + }, + "service_account_config": types.ObjectType{ + AttrTypes: ServiceProjectConfigServiceAccountConfigModel{}.AttributeTypes(), + }, + } +} + +type ServiceProjectConfigTenantProjectPolicyModel struct { + PolicyBindings types.List `tfsdk:"policy_bindings"` +} + +func (ServiceProjectConfigTenantProjectPolicyModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "policy_bindings": types.ListType{ + ElemType: types.ObjectType{AttrTypes: PolicyBinding{}.AttributeTypes()}, + }, + } +} + +type PolicyBinding struct { + Role types.String `tfsdk:"role"` + Members types.List `tfsdk:"members"` +} + +func (PolicyBinding) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "role": types.StringType, + "members": types.ListType{ElemType: types.StringType}, + } +} + +type ServiceProjectConfigBillingConfigModel struct { + BillingAccount types.String `tfsdk:"billing_account"` +} + +func (ServiceProjectConfigBillingConfigModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "billing_account": types.StringType, + } +} + +type ServiceProjectConfigServiceAccountConfigModel struct { + AccountID types.String `tfsdk:"account_id"` + TenantProjectRoles types.List `tfsdk:"tenant_project_roles"` +} + +func (ServiceProjectConfigServiceAccountConfigModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "account_id": types.StringType, + "tenant_project_roles": types.ListType{ElemType: types.StringType}, + } +} + +func (r *ServiceProjectResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_project" +} + +func (r *ServiceProjectResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "A service manager service.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the project.", + Computed: true, + }, + "tenancy_unit": schema.StringAttribute{ + MarkdownDescription: "The tenancy unit the project belongs to.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^services/[^/]+/[^/]+/[^/]+/tenancyUnits/[^/]+$"), "The tenancy unit must be in the format `services/{service_name}/{collection_id}/{resource_id}/tenancyUnits/{tenancy_unit_id}`."), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "tag": schema.StringAttribute{ + MarkdownDescription: "The tag to apply to the project.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "project_config": schema.SingleNestedAttribute{ + MarkdownDescription: "The project configuration.", + Required: true, + Attributes: map[string]schema.Attribute{ + "folder": schema.StringAttribute{ + MarkdownDescription: "Folder where project in this tenancy unit must be located This folder must have been previously created with the required permissions for the caller to create and configure a project in it. Valid folder resource names have the format folders/{folder_number} (for example, folders/123456).", + Required: true, + }, + "tenant_project_policy": schema.SingleNestedAttribute{ + MarkdownDescription: "Describes ownership and policies for the new tenant project. Required.", + Required: true, + Attributes: map[string]schema.Attribute{ + "policy_bindings": schema.ListNestedAttribute{ + MarkdownDescription: "Policy bindings to be applied to the tenant project, in addition to the 'roles/owner' role granted to the Service Consumer Management service account. At least one binding must have the role roles/owner. Among the list of members for roles/owner, at least one of them must be either the user or group type.", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "role": schema.StringAttribute{ + MarkdownDescription: "The role to which members will be added.", + Required: true, + }, + "members": schema.ListAttribute{ + MarkdownDescription: "The members to add to the role.", + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + }, + "labels": schema.MapAttribute{ + MarkdownDescription: "Labels to apply to the project.", + Optional: true, + ElementType: types.StringType, + }, + "services": schema.ListAttribute{ + MarkdownDescription: "Google Cloud API names of services that are activated on this project during provisioning. If any of these services can't be activated, the request fails. For example: 'compute.googleapis.com','cloudfunctions.googleapis.com'", + Optional: true, + ElementType: types.StringType, + }, + "billing_config": schema.SingleNestedAttribute{ + MarkdownDescription: "Billing account properties. The billing account must be specified.", + Required: true, + Attributes: map[string]schema.Attribute{ + "billing_account": schema.StringAttribute{ + MarkdownDescription: "Name of the billing account. For example billingAccounts/012345-567890-ABCDEF.", + Required: true, + }, + }, + }, + "service_account_config": schema.SingleNestedAttribute{ + MarkdownDescription: "Configuration for the IAM service account on the tenant project.", + Required: true, + Attributes: map[string]schema.Attribute{ + "account_id": schema.StringAttribute{ + MarkdownDescription: "ID of the IAM service account to be created in tenant project. The email format of the service account is \"@.iam.gserviceaccount.com\". This account ID must be unique within tenant project and service producers have to guarantee it. The ID must be 6-30 characters long, and match the following regular expression: [a-z]([-a-z0-9]*[a-z0-9]).", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([-a-z0-9]*[a-z0-9])$"), "The account ID must be 6-30 characters long and match the regular expression [a-z]([-a-z0-9]*[a-z0-9])."), + }, + }, + "tenant_project_roles": schema.ListAttribute{ + MarkdownDescription: "Roles for the associated service account for the tenant project.", + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + "status": schema.StringAttribute{ + MarkdownDescription: ` +Status: Status of tenant resource. + +Possible values: + "STATUS_UNSPECIFIED" - Unspecified status is the default unset value. + "PENDING_CREATE" - Creation of the tenant resource is ongoing. + "ACTIVE" - Active resource. + "PENDING_DELETE" - Deletion of the resource is ongoing. + "FAILED" - Tenant resource creation or deletion has failed. + "DELETED" - Tenant resource has been deleted.`, + Computed: true, + }, + }, + } +} + +func (r *ServiceProjectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + clients, ok := req.ProviderData.(*UtilsProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *UtilsProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.ServiceManagerClient = clients.ServiceManagerClient + r.TenantClient = clients.TenantClient + r.OperationsClient = clients.OperationsClient +} + +func (r *ServiceProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceProjectResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var projectConfigModel ServiceProjectConfigModel + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("project_config"), &projectConfigModel)...) + if resp.Diagnostics.HasError() { + return + } + + projectConfig := projectConfigModel.toProjectConfig(ctx, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + parent := data.TenancyUnit.ValueString() + op, err := r.TenantClient.Services.TenancyUnits.AddProject(parent, &serviceconsumermanagement.AddTenantProjectRequest{ + Tag: data.Tag.ValueString(), + ProjectConfig: projectConfig, + }).Context(ctx).Do() + + if err != nil { + resp.Diagnostics.AddError("Error adding project", err.Error()) + return + } + + for !op.Done { + time.Sleep(5 * time.Second) + + op, err = r.TenantClient.Operations.Get(op.Name).Context(ctx).Do() + if err != nil { + resp.Diagnostics.AddError("Error getting operation", err.Error()) + return + } + } + + project, err := r.getTenantProject(ctx, data.TenancyUnit.ValueString(), data.Tag.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error getting project", err.Error()) + return + } + if project == nil { + panic("project not found") + } + + data.ID = types.StringValue(project.Resource) + data.Status = types.StringValue(project.Status) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (projectConfigModel ServiceProjectConfigModel) toProjectConfig(ctx context.Context, diags diag.Diagnostics) *serviceconsumermanagement.TenantProjectConfig { + var tenantProjectPolicy serviceconsumermanagement.TenantProjectPolicy + if !projectConfigModel.TenantProjectPolicy.IsUnknown() && !projectConfigModel.TenantProjectPolicy.IsNull() { + var tenantProjectPolicyModel ServiceProjectConfigTenantProjectPolicyModel + diags.Append(projectConfigModel.TenantProjectPolicy.As(ctx, &tenantProjectPolicyModel, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil + } + policyBindingsValue, diags := tenantProjectPolicyModel.PolicyBindings.ToListValue(ctx) + if diags.HasError() { + diags.Append(diags...) + return nil + } + policyBindings := make([]PolicyBinding, len(policyBindingsValue.Elements())) + diags.Append(policyBindingsValue.ElementsAs(ctx, &policyBindings, false)...) + if diags.HasError() { + return nil + } + tenantProjectPolicy.PolicyBindings = make([]*serviceconsumermanagement.PolicyBinding, len(policyBindings)) + for i, policyBinding := range policyBindings { + var members []string + diags.Append(policyBinding.Members.ElementsAs(ctx, &members, false)...) + if diags.HasError() { + return nil + } + tenantProjectPolicy.PolicyBindings[i] = &serviceconsumermanagement.PolicyBinding{ + Role: policyBinding.Role.ValueString(), + Members: members, + } + } + } + + var labels map[string]string + diags.Append(projectConfigModel.Labels.ElementsAs(ctx, &labels, false)...) + if diags.HasError() { + return nil + } + + var services []string + diags.Append(projectConfigModel.Services.ElementsAs(ctx, &services, false)...) + if diags.HasError() { + return nil + } + + var billingConfig serviceconsumermanagement.BillingConfig + if !projectConfigModel.BillingConfig.IsUnknown() && !projectConfigModel.BillingConfig.IsNull() { + var billingConfigModel ServiceProjectConfigBillingConfigModel + diags.Append(projectConfigModel.BillingConfig.As(ctx, &billingConfigModel, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil + } + billingConfig.BillingAccount = billingConfigModel.BillingAccount.ValueString() + } + + var serviceAccountConfig serviceconsumermanagement.ServiceAccountConfig + if !projectConfigModel.ServiceAccountConfig.IsUnknown() && !projectConfigModel.ServiceAccountConfig.IsNull() { + var serviceAccountConfigModel ServiceProjectConfigServiceAccountConfigModel + diags.Append(projectConfigModel.ServiceAccountConfig.As(ctx, &serviceAccountConfigModel, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil + } + serviceAccountConfig.AccountId = serviceAccountConfigModel.AccountID.ValueString() + tenantProjectRolesValue, diags := serviceAccountConfigModel.TenantProjectRoles.ToListValue(ctx) + if diags.HasError() { + diags.Append(diags...) + return nil + } + tenantProjectRoles := make([]string, len(tenantProjectRolesValue.Elements())) + diags.Append(tenantProjectRolesValue.ElementsAs(ctx, &tenantProjectRoles, false)...) + if diags.HasError() { + return nil + } + serviceAccountConfig.TenantProjectRoles = tenantProjectRoles + } + + return &serviceconsumermanagement.TenantProjectConfig{ + Folder: projectConfigModel.Folder.ValueString(), + TenantProjectPolicy: &tenantProjectPolicy, + Labels: labels, + Services: services, + BillingConfig: &billingConfig, + ServiceAccountConfig: &serviceAccountConfig, + } +} + +func (r *ServiceProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceProjectResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + project, err := r.getTenantProject(ctx, data.TenancyUnit.ValueString(), data.Tag.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error getting project", err.Error()) + return + } + if project == nil { + return + } + + data.ID = types.StringValue(project.Resource) + data.Status = types.StringValue(project.Status) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data ServiceProjectResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var projectConfigModel ServiceProjectConfigModel + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("project_config"), &projectConfigModel)...) + if resp.Diagnostics.HasError() { + return + } + + projectConfig := projectConfigModel.toProjectConfig(ctx, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + op, err := r.TenantClient.Services.TenancyUnits.ApplyProjectConfig(data.TenancyUnit.ValueString(), &serviceconsumermanagement.ApplyTenantProjectConfigRequest{ + Tag: data.Tag.ValueString(), + ProjectConfig: projectConfig, + }).Context(ctx).Do() + + if err != nil { + resp.Diagnostics.AddError("Error updating project", err.Error()) + return + } + + for !op.Done { + time.Sleep(5 * time.Second) + + op, err = r.TenantClient.Operations.Get(op.Name).Context(ctx).Do() + if err != nil { + resp.Diagnostics.AddError("Error getting operation", err.Error()) + return + } + } + + project, err := r.getTenantProject(ctx, data.TenancyUnit.ValueString(), data.Tag.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error getting project", err.Error()) + return + } + if project == nil { + panic("project not found") + } + + data.ID = types.StringValue(project.Resource) + data.Status = types.StringValue(project.Status) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + +} + +func (r *ServiceProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data ServiceProjectResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + op, err := r.TenantClient.Services.TenancyUnits.RemoveProject(data.TenancyUnit.ValueString(), &serviceconsumermanagement.RemoveTenantProjectRequest{ + Tag: data.Tag.ValueString(), + }).Context(ctx).Do() + + if err != nil { + resp.Diagnostics.AddError("Error removing project", err.Error()) + return + } + + for !op.Done { + time.Sleep(5 * time.Second) + + op, err = r.TenantClient.Operations.Get(op.Name).Context(ctx).Do() + if err != nil { + resp.Diagnostics.AddError("Error getting operation", err.Error()) + return + } + } +} + +type TenantResource serviceconsumermanagement.TenantResource + +func (r TenantResource) ServiceAccountEmail() string { + resourceParts := strings.Split(r.Resource, "/") // projects/{project_id} + if resourceParts[0] != "projects" { + log.Panicf("unexpected resource type: %q", r.Resource) + } + return fmt.Sprintf("%s@%s.iam.gserviceaccount.com", r.Tag, resourceParts[1]) +} + +func (r *UtilsProviderConfig) getTenantProject(ctx context.Context, tenancyUnitID, tag string) (*TenantResource, error) { + tenancyUnit, err := r.getTenancyUnit(ctx, tenancyUnitID) + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound || strings.Contains(err.Error(), "not found") { + return nil, nil + } + return nil, err + } + if tenancyUnit == nil { + return nil, nil + } + for _, resource := range tenancyUnit.TenantResources { + if resource.Tag == tag { + return (*TenantResource)(resource), nil + } + } + return nil, nil +} diff --git a/internal/provider/resource_service_rollout.go b/internal/provider/resource_service_rollout.go new file mode 100644 index 0000000..df040ce --- /dev/null +++ b/internal/provider/resource_service_rollout.go @@ -0,0 +1,248 @@ +package provider + +import ( + "context" + "fmt" + + "cloud.google.com/go/servicemanagement/apiv1/servicemanagementpb" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ServiceRolloutResource{} + +func NewServiceRolloutResource() resource.Resource { + return &ServiceRolloutResource{} +} + +// ServiceResource defines the resource implementation. +type ServiceRolloutResource struct { + UtilsProviderConfig +} + +type ServiceRolloutResourceModel struct { + Id types.String `tfsdk:"id"` + ConfigId types.String `tfsdk:"config_id"` + RolloutConfig types.Map `tfsdk:"rollout_config"` +} + +func (r *ServiceRolloutResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_rollout" +} + +// Schema implements resource.Resource. +func (r *ServiceRolloutResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A service manager service rollout.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the rollout.", + Computed: true, + }, + "config_id": schema.StringAttribute{ + MarkdownDescription: "The ID of the config. Only one of `config_id` or `rollout_config` can be specified.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("config_id"), path.MatchRoot("rollout_config")), + }, + }, + "rollout_config": schema.MapAttribute{ + MarkdownDescription: "The rollout configuration by config ID. Only one of `config_id` or `rollout_config` can be specified.", + Optional: true, + ElementType: types.Float64Type, + Validators: []validator.Map{ + mapvalidator.ExactlyOneOf(path.MatchRoot("config_id"), path.MatchRoot("rollout_config")), + }, + }, + }, + } +} + +func (r *ServiceRolloutResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*UtilsProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *UtilsProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.ServiceManagerClient = config.ServiceManagerClient + r.OperationsClient = config.OperationsClient +} + +// Create implements resource.Resource. +func (r *ServiceRolloutResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceRolloutResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + rolloutId := r.createRollout(ctx, data, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + data.Id = *rolloutId + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Delete implements resource.Resource. +func (r *ServiceRolloutResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op. TF will remove from state, but GCP does not support deleting rollout. +} + +// Read implements resource.Resource. +func (r *ServiceRolloutResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceRolloutResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if data.Id.IsNull() { + return + } + + serviceName, rolloutId, err := parseRolloutId(data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid ID", err.Error()) + return + } + + rollout, err := r.ServiceManagerClient.GetServiceRollout(ctx, &servicemanagementpb.GetServiceRolloutRequest{ + ServiceName: serviceName, + RolloutId: rolloutId, + }) + + if err != nil { + if status, ok := status.FromError(err); ok && status.Code() == codes.NotFound { + return + } + resp.Diagnostics.AddError("Error reading service rollout", err.Error()) + return + } + + rawRolloutConfig := rollout.GetTrafficPercentStrategy().GetPercentages() + rolloutConfig, diags := types.MapValueFrom(ctx, types.Float64Type, rawRolloutConfig) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + if data.ConfigId.IsNull() && data.RolloutConfig.IsNull() { + if len(rawRolloutConfig) == 1 { + var configId string + for key := range rawRolloutConfig { + configId = key + } + // Populate the config ID. + data.ConfigId = newConfigId(serviceName, configId) + } else { + // Populate the rollout config. + data.RolloutConfig = rolloutConfig + } + } else if data.ConfigId.IsNull() { + data.RolloutConfig = rolloutConfig + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Update implements resource.Resource. +func (r *ServiceRolloutResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data ServiceRolloutResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + rolloutId := r.createRollout(ctx, data, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + data.Id = *rolloutId + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceRolloutResource) createRollout(ctx context.Context, data ServiceRolloutResourceModel, diagnostics diag.Diagnostics) *basetypes.StringValue { + var serviceName string + percentages := make(map[string]float64) + + if !data.ConfigId.IsNull() { + svc, configId, err := parseConfigId(data.ConfigId.ValueString()) + if err != nil { + diagnostics.AddError("Invalid config ID", err.Error()) + return nil + } + serviceName = svc + percentages[configId] = 100 + } else { + rawPercentages := make(map[string]float64) + diags := data.RolloutConfig.ElementsAs(ctx, &rawPercentages, false) + diagnostics.Append(diags...) + if diagnostics.HasError() { + return nil + } + for k, v := range rawPercentages { + svcName, configId, err := parseConfigId(k) + if err != nil { + diagnostics.AddError("Invalid config ID", err.Error()) + return nil + } + if serviceName == "" { + serviceName = svcName + } else if serviceName != svcName { + diagnostics.AddError("Invalid config ID", "All config IDs must be for the same service") + return nil + } + percentages[configId] = v + } + } + + // Create the rollout. + + rolloutOp, err := r.ServiceManagerClient.CreateServiceRollout(ctx, &servicemanagementpb.CreateServiceRolloutRequest{ + ServiceName: serviceName, + Rollout: &servicemanagementpb.Rollout{ + ServiceName: serviceName, + Strategy: &servicemanagementpb.Rollout_TrafficPercentStrategy_{ + TrafficPercentStrategy: &servicemanagementpb.Rollout_TrafficPercentStrategy{ + Percentages: percentages, + }, + }, + }, + }) + + if err != nil { + diagnostics.AddError("Error creating service rollout", err.Error()) + return nil + } + + rollout, err := rolloutOp.Wait(ctx) + if err != nil { + diagnostics.AddError("Error creating service rollout", err.Error()) + return nil + } + + rolloutId := newRolloutId(serviceName, rollout.RolloutId) + return &rolloutId +} diff --git a/internal/provider/resource_tenancy_unit.go b/internal/provider/resource_tenancy_unit.go new file mode 100644 index 0000000..5723afa --- /dev/null +++ b/internal/provider/resource_tenancy_unit.go @@ -0,0 +1,195 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "google.golang.org/api/serviceconsumermanagement/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ServiceTenancyUnitResource{} + +func NewServiceTenancyUnitResource() resource.Resource { + return &ServiceTenancyUnitResource{} +} + +// ServiceTenancyUnitResource defines the resource implementation. +type ServiceTenancyUnitResource struct { + UtilsProviderConfig +} + +// ServiceTenancyUnitModel describes the resource data model. +type ServiceTenancyUnitModel struct { + ID types.String `tfsdk:"id"` + ServiceName types.String `tfsdk:"service_name"` + Consumer types.String `tfsdk:"consumer"` +} + +func (r *ServiceTenancyUnitResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_tenancy_unit" +} + +func (r *ServiceTenancyUnitResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "A tenancy unit in a Service Manager service.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the tenancy unit.", + Optional: true, + Computed: true, + }, + "service_name": schema.StringAttribute{ + MarkdownDescription: "The name of the service.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "consumer": schema.StringAttribute{ + MarkdownDescription: "The consumer's ID, for example `projects/{project_number}`.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^projects/\d+$`), "Consumer must be `projects/{project_number}`"), + }, + }, + }, + } +} + +func (r *ServiceTenancyUnitResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + clients, ok := req.ProviderData.(*UtilsProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *UtilsProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.ServiceManagerClient = clients.ServiceManagerClient + r.TenantClient = clients.TenantClient + r.OperationsClient = clients.OperationsClient +} + +func (r *ServiceTenancyUnitResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceTenancyUnitModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var id string + if !data.ID.IsUnknown() && !data.ID.IsNull() { + id = data.ID.ValueString() + } + + parent := fmt.Sprintf("services/%s/%s", data.ServiceName.ValueString(), data.Consumer.ValueString()) + tenancyUnit, err := r.TenantClient.Services.TenancyUnits.Create(parent, &serviceconsumermanagement.CreateTenancyUnitRequest{ + TenancyUnitId: id, + }).Context(ctx).Do() + if err != nil { + resp.Diagnostics.AddError("Error creating tenancy unit", err.Error()) + return + } + + data.ID = types.StringValue(tenancyUnit.Name) + + // Write the updated model back to Terraform + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceTenancyUnitResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceTenancyUnitModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + tenancyUnit, err := r.getTenancyUnit(ctx, data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error getting tenancy unit", err.Error()) + return + } + + if tenancyUnit == nil { + return + } + + data.ID = types.StringValue(tenancyUnit.Name) + data.ServiceName = types.StringValue(tenancyUnit.Service) + data.Consumer = types.StringValue(tenancyUnit.Consumer) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceTenancyUnitResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + panic("Updating a service is not supported") +} + +func (r *ServiceTenancyUnitResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data ServiceTenancyUnitModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + _, err := r.TenantClient.Services.TenancyUnits.Delete(data.ID.ValueString()).Context(ctx).Do() + if err != nil { + resp.Diagnostics.AddError("Error deleting tenancy unit", err.Error()) + return + } +} + +func (p *UtilsProviderConfig) getTenancyUnit(ctx context.Context, id string) (*serviceconsumermanagement.TenancyUnit, error) { + parent := strings.Split(id, "/tenancyUnits/")[0] + tenancyUnits, err := p.TenantClient.Services.TenancyUnits.List(parent).Context(ctx).Do() + if err != nil { + if err, ok := status.FromError(err); ok && (err.Code() == codes.NotFound || strings.Contains(err.String(), "not found")) { + return nil, nil + } + return nil, err + } + + var tenancyUnit *serviceconsumermanagement.TenancyUnit + for _, tu := range tenancyUnits.TenancyUnits { + if strings.EqualFold(tu.Name, id) { + tenancyUnit = tu + break + } + } + + return tenancyUnit, nil +} diff --git a/internal/provider/util.go b/internal/provider/util.go new file mode 100644 index 0000000..a20e613 --- /dev/null +++ b/internal/provider/util.go @@ -0,0 +1,32 @@ +package provider + +import ( + "errors" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func parseConfigId(id string) (string, string, error) { + parts := strings.Split(id, "/") + if len(parts) != 2 { + return "", "", errors.New("ID must be in the format `{serviceName}/{configId}`") + } + return parts[0], parts[1], nil +} + +func newConfigId(serviceName, configId string) types.String { + return types.StringValue(serviceName + "/" + configId) +} + +func parseRolloutId(id string) (string, string, error) { + parts := strings.Split(id, "/") + if len(parts) != 2 { + return "", "", errors.New("ID must be in the format `{serviceName}/{rolloutId}`") + } + return parts[0], parts[1], nil +} + +func newRolloutId(serviceName, rolloutId string) types.String { + return types.StringValue(serviceName + "/" + rolloutId) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f94437c --- /dev/null +++ b/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "flag" + "log" + + "github.com/celest-dev/terraform-provider-utils/internal/provider" + "github.com/hashicorp/terraform-plugin-framework/providerserver" +) + +// Run "go generate" to format example terraform files and generate the docs for the registry/website + +// If you do not have terraform installed, you can remove the formatting command, but its suggested to +// ensure the documentation is formatted properly. +//go:generate terraform fmt -recursive ./examples/ + +// Run the docs generation tool, check its repository for more information on how it works and how docs +// can be customized. +//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate -provider-name utils + +var ( + // these will be set by the goreleaser configuration + // to appropriate values for the compiled binary. + version string = "dev" + + // goreleaser can pass other information to the main package, such as the specific commit + // https://goreleaser.com/cookbooks/using-main.version/ +) + +func main() { + var debug bool + + flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + + opts := providerserver.ServeOpts{ + Address: "registry.terraform.io/celest-dev/utils", + ProtocolVersion: 6, + Debug: debug, + } + + err := providerserver.Serve(context.Background(), provider.New(version), opts) + + if err != nil { + log.Fatal(err) + } +} diff --git a/terraform-registry-manifest.json b/terraform-registry-manifest.json new file mode 100644 index 0000000..047a8e8 --- /dev/null +++ b/terraform-registry-manifest.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "metadata": { + "protocol_versions": [ + "6.0" + ] + } +} \ No newline at end of file diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..2c4f8fb --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,8 @@ +//go:build tools + +package tools + +import ( + // Documentation generation + _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" +)