diff --git a/.github/images/logo-full.svg b/.github/images/logo-full.svg
new file mode 100644
index 0000000..c1e5864
--- /dev/null
+++ b/.github/images/logo-full.svg
@@ -0,0 +1,89 @@
+
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..95bc764
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,34 @@
+name: Go Lint and Format
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "main"
+ - "master"
+
+env:
+ GO_VERSION: stable
+ GOLANGCI_LINT_VERSION: v1.60
+
+jobs:
+ golangci-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ - name: golangci-lint Action
+ uses: golangci/golangci-lint-action@v6
+ with:
+ args: --timeout=3m
+
+ terraform-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: hashicorp/setup-terraform@v3.1.2
+ - name: Terraform Accepatance Tests
+ run: |
+ DFCLOUD_API_KEY=${{ secrets.DFCLOUD_API_KEY }} make test
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..bea4ce8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,49 @@
+# 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*'
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "The version of the release"
+ type: string
+ required: false
+
+# Releases need permissions to read and write the repository contents.
+# GitHub considers creating releases and uploading assets as writing contents.
+permissions:
+ contents: write
+
+env:
+ GO_VERSION: stable
+
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ cache: true
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v6
+ id: import_gpg
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ passphrase: ${{ secrets.PASSPHRASE }}
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ args: release --clean
+ env:
+ GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
+ VERSION: ${{ github.event.inputs.version || github.ref }}
+
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..b105516
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,64 @@
+# Visit https://goreleaser.com for documentation on how to customize this
+# behavior.
+version: 2
+before:
+ hooks:
+ # this is just an example and not a requirement for provider building/publishing
+ - go mod tidy
+builds:
+- env:
+ # goreleaser does not work with CGO, it could also complicate
+ # usage by users in CI/CD systems like HCP Terraform 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:
+ github:
+ owner: dragonflydb
+ name: terraform-provider-dfcloud
+ 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/Makefile b/Makefile
new file mode 100644
index 0000000..7b440d4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,25 @@
+PLUGIN_NAME := terraform-provider-dfcloud
+PLUGIN_PATH := ./bin/
+
+build:
+ @echo "Building Terraform plugin..."
+ goreleaser build --single-target --skip=validate --clean --snapshot
+
+install-tfplugindocs:
+ @echo "Installing tfplugindocs tool..."
+ @go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.19.4
+
+docs: install-tfplugindocs
+ @echo "Updating documentation..."
+ @tfplugindocs generate --provider-name dfcloud
+
+.PHONY: install-tfplugindocs install-goreleaser docs
+
+test:
+ @echo "Run acceptance tests against the provider"
+ TF_ACC=true go test ./... $(CLI_ARGS)
+
+update-terraformrc:
+ @echo 'provider_installation {\n dev_overrides {\n "registry.terraform.io/dragonflydb/dfcloud" = "$(PWD)/bin"\n "registry.terraform.io/hashicorp/aws" = "$(PWD)/bin"\n }\n\n # For all other providers, install them directly from their origin provider\n # registries as normal. If you omit this, Terraform will _only_ use\n # the dev_overrides block, and so no other providers will be available.\n direct {}\n}' > ~/.terraformrc
+
+.PHONY: build install update-terraformrc
diff --git a/README.md b/README.md
index cbf6468..3977f6c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,12 @@
# terraform-provider-dfcloud
+
+
+
+
+
+
Dragonfly Cloud terraform provider is a terraform provider for managing resources in [Dragonfly Cloud](https://dragonflydb.cloud/).
This provider is currently in *beta*. Feel free to try it out and provide feedback through issues.
diff --git a/docs/resources/datastore.md b/docs/resources/datastore.md
index 95ab489..0446722 100644
--- a/docs/resources/datastore.md
+++ b/docs/resources/datastore.md
@@ -3,12 +3,12 @@
page_title: "dfcloud_datastore Resource - dfcloud"
subcategory: ""
description: |-
- Manages a Dragonfly datastore resource.
+ Manages a Dragonfly datastore.
---
# dfcloud_datastore (Resource)
-Manages a Dragonfly datastore resource.
+Manages a Dragonfly datastore.
@@ -30,7 +30,7 @@ Manages a Dragonfly datastore resource.
- `addr` (String) The address of the datastore.
- `created_at` (Number) The timestamp when the datastore was created.
-- `id` (String) The ID of the datastore.
+- `datastore_id` (String) The ID of the datastore.
- `password` (String, Sensitive) The password for the datastore.
diff --git a/docs/resources/network.md b/docs/resources/network.md
index 19503e5..488bd3c 100644
--- a/docs/resources/network.md
+++ b/docs/resources/network.md
@@ -26,7 +26,7 @@ Manages a Dragonfly network.
- `created_at` (Number) The timestamp when the network was created.
- `id` (String) The ID of the network.
- `status` (String) The status of the network.
-- `vpc` (Attributes) The VPC information for the network. (see [below for nested schema](#nestedatt--vpc))
+- `vpc` (Object) The VPC information for the network. (see [below for nested schema](#nestedatt--vpc))
### Nested Schema for `location`
@@ -42,5 +42,5 @@ Required:
Read-Only:
-- `account_id` (String) The account ID of the VPC.
-- `resource_id` (String) The resource ID of the VPC.
+- `account_id` (String)
+- `resource_id` (String)
diff --git a/examples/network/main.tf b/examples/network/main.tf
new file mode 100644
index 0000000..c98862b
--- /dev/null
+++ b/examples/network/main.tf
@@ -0,0 +1,62 @@
+terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "5.50.0"
+ }
+
+ dfcloud = {
+ source = "registry.terraform.io/dragonflydb/dfcloud"
+ }
+ }
+}
+
+provider "aws" {
+ # Configuration options
+}
+
+provider "dfcloud" {
+ # Configuration options
+}
+
+data "aws_caller_identity" "current" {}
+
+# client VPC
+resource "aws_vpc" "client" {
+ cidr_block = "10.0.0.0/16"
+
+ tags = {
+ Name = "client"
+ }
+}
+
+# private network
+resource "dfcloud_network" "network" {
+ name = "network"
+ location = {
+ region = "us-east-1"
+ provider = "aws"
+ }
+ cidr_block = "192.168.0.0/16"
+}
+
+# private connection
+resource "dfcloud_connection" "connection" {
+ depends_on = [aws_vpc.client, dfcloud_network.network]
+
+ name = "connection"
+ peer = {
+ account_id = data.aws_caller_identity.current.account_id
+ region = "us-east-1"
+ vpc_id = aws_vpc.client.id
+ }
+
+ network_id = dfcloud_network.network.id
+}
+
+resource "aws_vpc_peering_connection_accepter" "accepter" {
+ depends_on = [dfcloud_connection.connection]
+
+ vpc_peering_connection_id = dfcloud_connection.connection.peer_connection_id
+ auto_accept = true
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..534473e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,65 @@
+module github.com/dragonflydb/terraform-provider-dfcloud
+
+go 1.22.0
+
+toolchain go1.22.10
+
+require (
+ github.com/hashicorp/terraform-plugin-framework v1.8.0
+ github.com/hashicorp/terraform-plugin-go v0.25.0
+ github.com/hashicorp/terraform-plugin-log v0.9.0
+ github.com/hashicorp/terraform-plugin-testing v1.11.0
+ github.com/samber/lo v1.47.0
+)
+
+require (
+ github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
+ github.com/agext/levenshtein v1.2.2 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/fatih/color v1.16.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/hashicorp/errwrap v1.0.0 // indirect
+ github.com/hashicorp/go-checkpoint v0.5.0 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // 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.2 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.7 // 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.9.0 // indirect
+ github.com/hashicorp/hcl/v2 v2.23.0 // indirect
+ github.com/hashicorp/logutils v1.0.0 // indirect
+ github.com/hashicorp/terraform-exec v0.21.0 // indirect
+ github.com/hashicorp/terraform-json v0.23.0 // indirect
+ github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.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/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-testing-interface v1.14.1 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.0 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/oklog/run v1.0.0 // indirect
+ github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
+ github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ github.com/zclconf/go-cty v1.15.0 // indirect
+ golang.org/x/crypto v0.29.0 // indirect
+ golang.org/x/mod v0.21.0 // indirect
+ golang.org/x/net v0.28.0 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/sys v0.27.0 // indirect
+ golang.org/x/text v0.20.0 // indirect
+ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
+ google.golang.org/grpc v1.67.1 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..dc663b0
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,220 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+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/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
+github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
+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/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/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
+github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
+github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
+github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
+github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+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/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.5.5/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/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.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-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
+github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs=
+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.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.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
+github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+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.9.0 h1:2dIk8LcvANwtv3QZLckxcjyF5w8KVtiMxu6G6eLhghE=
+github.com/hashicorp/hc-install v0.9.0/go.mod h1:+6vOP+mf3tuGgMApVYtmsnDoKWMDcFXeTxCACYZ8SFg=
+github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
+github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
+github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+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.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=
+github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
+github.com/hashicorp/terraform-plugin-framework v1.8.0 h1:P07qy8RKLcoBkCrY2RHJer5AEvJnDuXomBgou6fD8kI=
+github.com/hashicorp/terraform-plugin-framework v1.8.0/go.mod h1:/CpTukO88PcL/62noU7cuyaSJ4Rsim+A/pa+3rUVufY=
+github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks=
+github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw=
+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-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOWlNfYXfXpVo16iDyLp8Y=
+github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw=
+github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A=
+github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U=
+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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+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/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/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+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/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
+github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
+github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
+github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+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-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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.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.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
+golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+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/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+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.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/provider/connection.go b/internal/provider/connection.go
new file mode 100644
index 0000000..ec77da3
--- /dev/null
+++ b/internal/provider/connection.go
@@ -0,0 +1,191 @@
+package provider
+
+import (
+ "context"
+ "time"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/dragonflydb/terraform-provider-dfcloud/internal/resource_model"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ConnectionResource struct {
+ client *dfcloud.Client
+}
+
+func NewConnectionResource() resource.Resource {
+ return &ConnectionResource{}
+}
+
+func (r *ConnectionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = "dfcloud_connection"
+}
+
+func (r *ConnectionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a Dragonfly network connection.",
+ Attributes: map[string]schema.Attribute{
+ "connection_id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the connection.",
+ Computed: true,
+ },
+ "status": schema.StringAttribute{
+ MarkdownDescription: "The status of the connection.",
+ Computed: true,
+ },
+ "status_detail": schema.StringAttribute{
+ MarkdownDescription: "Additional details about the connection status.",
+ Computed: true,
+ },
+ "peer_connection_id": schema.StringAttribute{
+ MarkdownDescription: "The underlying cloud provider connection ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the connection.",
+ Required: true,
+ },
+ "network_id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the network to connect to.",
+ Required: true,
+ },
+ "peer": schema.SingleNestedAttribute{
+ MarkdownDescription: "The VPC to connect to.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "account_id": schema.StringAttribute{
+ MarkdownDescription: "The account ID of the target VPC.",
+ Required: true,
+ },
+ "vpc_id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the target VPC.",
+ Required: true,
+ },
+ "region": schema.StringAttribute{
+ MarkdownDescription: "The region of the target VPC.",
+ Optional: true,
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *ConnectionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*dfcloud.Client)
+ if !ok {
+ resp.Diagnostics.AddError("failed to get provider", "failed to get provider")
+ }
+
+ r.client = client
+}
+
+func (r *ConnectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var state resource_model.Connection
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ resp.Diagnostics.AddError("failed to get plan into state", "failed to get plan into state")
+ }
+
+ connConfig := resource_model.IntoConnectionConfig(state)
+ respConn, err := r.client.CreateConnection(ctx, connConfig)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create connection", err.Error())
+ return
+ }
+
+ // wait until VPC IDs are created
+ waitForConnectionStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ respConn, err = resource_model.WaitUntilConnectionStatus(waitForConnectionStatusCtx, r.client, respConn.ID, dfcloud.ConnectionStatusInactive)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to wait for connection", err.Error())
+ return
+ }
+
+ state.ConnectionID = types.StringValue(respConn.ID)
+ state.Status = types.StringValue(string(respConn.Status))
+ state.StatusDetail = types.StringValue(respConn.StatusDetail)
+ state.PeerConnID = types.StringValue(respConn.PeerConnectionID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *ConnectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state resource_model.Connection
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ respConn, err := r.client.GetConnection(ctx, state.ConnectionID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("failed to read connection", err.Error())
+ return
+ }
+
+ if respConn.Status == dfcloud.ConnectionStatusDeleted {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ state.ConnectionID = types.StringValue(respConn.ID)
+ state.Status = types.StringValue(string(respConn.Status))
+ state.StatusDetail = types.StringValue(respConn.StatusDetail)
+ state.PeerConnID = types.StringValue(respConn.PeerConnectionID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ConnectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // Connections can't be updated
+ resp.Diagnostics.AddError(
+ "Updating a Connection is not supported",
+ "Updating a Connection is not supported",
+ )
+}
+
+func (r *ConnectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state *resource_model.Connection
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteConnection(ctx, state.ConnectionID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("failed to delete connection", err.Error())
+ }
+
+ // wait until connection is deleted
+ waitForConnectionStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ _, err = resource_model.WaitUntilConnectionStatus(waitForConnectionStatusCtx, r.client, state.ConnectionID.ValueString(), dfcloud.ConnectionStatusDeleted)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to wait for connection", err.Error())
+ }
+
+}
+
+func (r *ConnectionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ connection, err := r.client.GetConnection(ctx, req.ID)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to get network", err.Error())
+ return
+ }
+
+ state := resource_model.FromConnectionConfig(connection)
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+var (
+ _ resource.Resource = &ConnectionResource{}
+ _ resource.ResourceWithImportState = &ConnectionResource{}
+)
diff --git a/internal/provider/datastore.go b/internal/provider/datastore.go
new file mode 100644
index 0000000..bd618e6
--- /dev/null
+++ b/internal/provider/datastore.go
@@ -0,0 +1,323 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/dragonflydb/terraform-provider-dfcloud/internal/resource_model"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+// NewDatastoreResource is a helper function to simplify the provider implementation.
+func NewDatastoreResource() resource.Resource {
+ return &datastoreResource{}
+}
+
+// datastoreResource is the resource implementation.
+type datastoreResource struct {
+ client *dfcloud.Client
+}
+
+// Metadata returns the resource type name.
+func (r *datastoreResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "datastore"
+}
+
+// Schema defines the schema for the resource.
+func (r *datastoreResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a Dragonfly datastore resource.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the datastore.",
+ Computed: true,
+ },
+ "created_at": schema.Int64Attribute{
+ MarkdownDescription: "The timestamp when the datastore was created.",
+ Computed: true,
+ },
+ "password": schema.StringAttribute{
+ MarkdownDescription: "The password for the datastore.",
+ Computed: true,
+ Sensitive: true,
+ },
+ "addr": schema.StringAttribute{
+ MarkdownDescription: "The address of the datastore.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the datastore.",
+ Required: true,
+ },
+ "location": schema.SingleNestedAttribute{
+ MarkdownDescription: "The location configuration for the datastore.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "provider": schema.StringAttribute{
+ MarkdownDescription: "The provider for the datastore location.",
+ Required: true,
+ },
+ "region": schema.StringAttribute{
+ MarkdownDescription: "The region for the datastore location.",
+ Required: true,
+ },
+ "availability_zones": schema.ListAttribute{
+ MarkdownDescription: "The availability zones for the datastore location.",
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ },
+ },
+ },
+ "tier": schema.SingleNestedAttribute{
+ MarkdownDescription: "The tier configuration for the datastore.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "max_memory_bytes": schema.Int64Attribute{
+ MarkdownDescription: "The maximum memory (in bytes) for the datastore.",
+ Required: true,
+ },
+ "performance_tier": schema.StringAttribute{
+ MarkdownDescription: "The performance tier for the datastore.",
+ Required: true,
+ },
+ "replicas": schema.Int64Attribute{
+ MarkdownDescription: "The number of replicas for the datastore.",
+ Optional: true,
+ },
+ },
+ },
+ "network_id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the network the datastore should be placed into.",
+ Optional: true,
+ },
+ "dragonfly": schema.SingleNestedAttribute{
+ MarkdownDescription: "Dragonfly-specific configuration.",
+ Optional: true,
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "cache_mode": schema.BoolAttribute{
+ MarkdownDescription: "Enable cache mode for memory management.",
+ Optional: true,
+ Computed: true,
+ },
+ "bullmq": schema.BoolAttribute{
+ MarkdownDescription: "Enable BullMQ compatibility.",
+ Optional: true,
+ Computed: true,
+ },
+ "tls": schema.BoolAttribute{
+ MarkdownDescription: "Enable TLS.",
+ Optional: true,
+ Computed: true,
+ },
+ "sidekiq": schema.BoolAttribute{
+ MarkdownDescription: "Enable Sidekiq compatibility.",
+ Optional: true,
+ Computed: true,
+ },
+ "memcached": schema.BoolAttribute{
+ MarkdownDescription: "Enable Memcached protocol.",
+ Optional: true,
+ Computed: true,
+ },
+ "acl_rules": schema.ListAttribute{
+ MarkdownDescription: "List of ACL rules.",
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ },
+ },
+ },
+ },
+ }
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *datastoreResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*dfcloud.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Provider Data Type",
+ fmt.Sprintf("Expected *dfcloud.Client, got: %T", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = client
+}
+
+// Create a new resource.
+func (r *datastoreResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan resource_model.Datastore
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ datastore := resource_model.IntoDatastoreConfig(plan)
+ if datastore == nil {
+ resp.Diagnostics.AddError("Configuration Error", "Failed to create datastore configuration")
+ return
+ }
+
+ respDatastore, err := r.client.CreateDatastore(ctx, &datastore.Config)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Creating Datastore", err.Error())
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ respDatastore, err = resource_model.WaitForDatastoreStatus(ctx, r.client, respDatastore.ID, dfcloud.DatastoreStatusActive)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Creating Datastore", err.Error())
+ return
+ }
+
+ tflog.Info(ctx, "created datastore", map[string]interface{}{
+ "datastore_id": respDatastore.ID,
+ "status": respDatastore.Status,
+ })
+
+ plan.FromConfig(respDatastore)
+ diags = resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+// Read resource information.
+func (r *datastoreResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state resource_model.Datastore
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ respDatastore, err := r.client.GetDatastore(ctx, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Datastore", err.Error())
+ return
+ }
+
+ if respDatastore.Status == dfcloud.DatastoreStatusDeleted {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ tflog.Info(ctx, "read datastore", map[string]interface{}{
+ "datastore_id": respDatastore.ID,
+ "status": respDatastore.Status,
+ })
+
+ state.FromConfig(respDatastore)
+ diags = resp.State.Set(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+}
+
+// Update resource information.
+func (r *datastoreResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var state resource_model.Datastore
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // retreive datastore to check if it is active
+ respDatastore, err := r.client.GetDatastore(ctx, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error Updating Datastore", err.Error())
+ }
+
+ if respDatastore.Status == dfcloud.DatastoreStatusUpdating || respDatastore.Status == dfcloud.DatastoreStatusPending || respDatastore.Status == dfcloud.DatastoreStatusDeleting {
+ resp.Diagnostics.AddError("Error Reading Datastore", "Datastore is not active")
+ }
+
+ var plan resource_model.Datastore
+ diags = req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ datastore := resource_model.IntoDatastoreConfig(plan)
+ respDatastore, err = r.client.UpdateDatastore(ctx, state.ID.ValueString(), &datastore.Config)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Updating Datastore", err.Error())
+ return
+ }
+
+ waitForDatastoreStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ respDatastore, err = resource_model.WaitForDatastoreStatus(waitForDatastoreStatusCtx, r.client, respDatastore.ID, dfcloud.DatastoreStatusActive)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Waiting for Datastore Update", err.Error())
+ return
+ }
+
+ tflog.Info(ctx, "updated datastore", map[string]interface{}{
+ "datastore_id": respDatastore.ID,
+ "status": respDatastore.Status,
+ })
+
+ plan.FromConfig(respDatastore)
+ diags = resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *datastoreResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state resource_model.Datastore
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteDatastore(ctx, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error Deleting Datastore", err.Error())
+ }
+
+ waitForDatastoreStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ _, err = resource_model.WaitForDatastoreStatus(waitForDatastoreStatusCtx, r.client, state.ID.ValueString(), dfcloud.DatastoreStatusDeleted)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Deleting Datastore", err.Error())
+ }
+
+ tflog.Info(ctx, "deleted datastore", map[string]interface{}{
+ "datastore_id": state.ID.ValueString(),
+ })
+}
+
+// ImportState imports the resource state from an external system.
+func (r *datastoreResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ datastore, err := r.client.GetDatastore(ctx, req.ID)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Importing Datastore", err.Error())
+ return
+ }
+
+ var plan resource_model.Datastore
+ plan.FromConfig(datastore)
+ diags := resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+var (
+ _ resource.Resource = &datastoreResource{}
+ _ resource.ResourceWithConfigure = &datastoreResource{}
+ _ resource.ResourceWithImportState = &datastoreResource{}
+)
diff --git a/internal/provider/datastore_test.go b/internal/provider/datastore_test.go
new file mode 100644
index 0000000..0f68d2a
--- /dev/null
+++ b/internal/provider/datastore_test.go
@@ -0,0 +1,109 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func testCheckDatastoreExists(n string) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[n]
+ if !ok {
+ return fmt.Errorf("not found: %s", n)
+ }
+
+ if rs.Primary.ID == "" {
+ return fmt.Errorf("no datastore ID is set")
+ }
+
+ _, err := testClient().GetDatastore(context.Background(), rs.Primary.ID)
+ if err != nil {
+ return fmt.Errorf("error fetching datastore with ID %s: %s", rs.Primary.ID, err)
+ }
+
+ return nil
+ }
+}
+
+func testCheckDatastoreDestroy(s *terraform.State) error {
+ client := testClient()
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "dfcloud_datastore" {
+ continue
+ }
+
+ ds, err := client.GetDatastore(context.Background(), rs.Primary.ID)
+ if err != nil {
+ return fmt.Errorf("error fetching datastore with ID %s: %s", rs.Primary.ID, err)
+ }
+
+ if ds.Status != dfcloud.DatastoreStatusDeleting && ds.Status != dfcloud.DatastoreStatusDeleted {
+ return fmt.Errorf("datastore still exists")
+ }
+ }
+
+ return nil
+}
+
+func TestAcc_DatastoreResource(t *testing.T) {
+ name := "tf-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ CheckDestroy: testCheckDatastoreDestroy,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: testAccDatastoreResourceConfig(name),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testCheckDatastoreExists("dfcloud_datastore.test"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "name", name),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "location.provider", "aws"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "location.region", "us-east-1"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "tier.performance_tier", "dev"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "tier.max_memory_bytes", "3000000000"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "tier.replicas", "1"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "dragonfly.cache_mode", "false"),
+ resource.TestCheckResourceAttr("dfcloud_datastore.test", "dragonfly.tls", "false"),
+ resource.TestCheckResourceAttrSet("dfcloud_datastore.test", "id"),
+ resource.TestCheckResourceAttrSet("dfcloud_datastore.test", "addr"),
+ resource.TestCheckResourceAttrSet("dfcloud_datastore.test", "created_at"),
+ resource.TestCheckResourceAttrSet("dfcloud_datastore.test", "password"),
+ ),
+ },
+ // Import State
+ {
+ ResourceName: "dfcloud_datastore.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ },
+ })
+}
+
+func testAccDatastoreResourceConfig(name string) string {
+ return fmt.Sprintf(`
+resource "dfcloud_datastore" "test" {
+ name = %[1]q
+
+ location = {
+ provider = "aws"
+ region = "us-east-1"
+ }
+
+ tier = {
+ max_memory_bytes = 3000000000 # 3GB
+ performance_tier = "dev"
+ replicas = 1
+ }
+}
+`, name)
+}
diff --git a/internal/provider/network.go b/internal/provider/network.go
new file mode 100644
index 0000000..58513bc
--- /dev/null
+++ b/internal/provider/network.go
@@ -0,0 +1,189 @@
+package provider
+
+import (
+ "context"
+ "time"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/dragonflydb/terraform-provider-dfcloud/internal/resource_model"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+)
+
+type NetworkResource struct {
+ client *dfcloud.Client
+}
+
+func NewNetworkResource() resource.Resource {
+ return &NetworkResource{}
+}
+
+func (r *NetworkResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = "dfcloud_network"
+}
+
+func (r *NetworkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a Dragonfly network.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The ID of the network.",
+ Computed: true,
+ },
+ "created_at": schema.Int64Attribute{
+ MarkdownDescription: "The timestamp when the network was created.",
+ Computed: true,
+ },
+ "status": schema.StringAttribute{
+ MarkdownDescription: "The status of the network.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the network.",
+ Required: true,
+ },
+ "location": schema.SingleNestedAttribute{
+ MarkdownDescription: "The location configuration for the network.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "provider": schema.StringAttribute{
+ MarkdownDescription: "The provider for the network location.",
+ Required: true,
+ },
+ "region": schema.StringAttribute{
+ MarkdownDescription: "The region for the network location.",
+ Required: true,
+ },
+ },
+ },
+ "cidr_block": schema.StringAttribute{
+ MarkdownDescription: "The CIDR block for the network.",
+ Required: true,
+ },
+ "vpc": schema.SingleNestedAttribute{
+ MarkdownDescription: "The VPC information for the network.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_id": schema.StringAttribute{
+ MarkdownDescription: "The resource ID of the VPC.",
+ Computed: true,
+ },
+ "account_id": schema.StringAttribute{
+ MarkdownDescription: "The account ID of the VPC.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *NetworkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*dfcloud.Client)
+ if !ok {
+ resp.Diagnostics.AddError("failed to get provider", "failed to get provider")
+ }
+
+ r.client = client
+}
+
+func (r *NetworkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var state resource_model.Network
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ resp.Diagnostics.AddError("failed to get plan into state", "failed to get plan into state")
+ }
+
+ networkConfig := resource_model.IntoNetworkConfig(state)
+ respNetwork, err := r.client.CreateNetwork(ctx, networkConfig)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create network", err.Error())
+ return
+ }
+
+ // wait until VPC IDs are created
+ waitForNetworkStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ respNetwork, err = resource_model.WaitUntilNetworkStatus(waitForNetworkStatusCtx, r.client, respNetwork.ID, dfcloud.NetworkStatusActive)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to wait for network", err.Error())
+ return
+ }
+
+ state = *resource_model.FromNetworkConfig(respNetwork)
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *NetworkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state resource_model.Network
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ waitForNetworkStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+ respNetwork, err := resource_model.WaitUntilNetworkStatus(waitForNetworkStatusCtx, r.client, state.Id.ValueString(), dfcloud.NetworkStatusActive)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to wait for network", err.Error())
+ return
+ }
+
+ if respNetwork.Status == dfcloud.NetworkStatusDeleted {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ state = *resource_model.FromNetworkConfig(respNetwork)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NetworkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // no updates allowed for networks
+ resp.Diagnostics.AddError(
+ "Updating a Network is not supported",
+ "Updating a Network is not supported",
+ )
+}
+
+func (r *NetworkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state *resource_model.Network
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteNetwork(ctx, state.Id.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("failed to delete network", err.Error())
+ }
+
+ // wait until network is deleted
+ waitForNetworkStatusCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
+ defer cancel()
+
+ _, err = resource_model.WaitUntilNetworkStatus(waitForNetworkStatusCtx, r.client, state.Id.ValueString(), dfcloud.NetworkStatusDeleted)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to wait for network deletion", err.Error())
+ }
+}
+
+func (r *NetworkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ network, err := r.client.GetNetwork(ctx, req.ID)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to get network", err.Error())
+ return
+ }
+
+ state := resource_model.FromNetworkConfig(network)
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+var (
+ _ resource.Resource = &NetworkResource{}
+ _ resource.ResourceWithImportState = &NetworkResource{}
+)
diff --git a/internal/provider/network_test.go b/internal/provider/network_test.go
new file mode 100644
index 0000000..4581c6c
--- /dev/null
+++ b/internal/provider/network_test.go
@@ -0,0 +1,102 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func testCheckNetworkExists(n string) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[n]
+ if !ok {
+ return fmt.Errorf("not found: %s", n)
+ }
+
+ if rs.Primary.ID == "" {
+ return fmt.Errorf("no network ID is set")
+ }
+
+ _, err := testClient().GetNetwork(context.Background(), rs.Primary.ID)
+ if err != nil {
+ return fmt.Errorf("error fetching network with ID %s: %s", rs.Primary.ID, err)
+ }
+
+ return nil
+ }
+}
+
+func testCheckNetworkDestroy(s *terraform.State) error {
+ client := testClient()
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "dfcloud_network" {
+ continue
+ }
+
+ network, err := client.GetNetwork(context.Background(), rs.Primary.ID)
+ if err != nil {
+ return fmt.Errorf("error fetching network with ID %s: %s", rs.Primary.ID, err)
+ }
+
+ if network.Status != dfcloud.NetworkStatusDeleting && network.Status != dfcloud.NetworkStatusDeleted {
+ return fmt.Errorf("network still exists")
+ }
+ }
+
+ return nil
+}
+
+func TestAcc_NetworkResource(t *testing.T) {
+ name := "tf-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ CheckDestroy: testCheckNetworkDestroy,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: testAccNetworkResourceConfig(name),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testCheckNetworkExists("dfcloud_network.test"),
+ resource.TestCheckResourceAttr("dfcloud_network.test", "name", name),
+ resource.TestCheckResourceAttr("dfcloud_network.test", "location.provider", "aws"),
+ resource.TestCheckResourceAttr("dfcloud_network.test", "location.region", "us-east-1"),
+ resource.TestCheckResourceAttr("dfcloud_network.test", "cidr_block", "10.0.0.0/16"),
+ resource.TestCheckResourceAttrSet("dfcloud_network.test", "id"),
+ resource.TestCheckResourceAttrSet("dfcloud_network.test", "created_at"),
+ resource.TestCheckResourceAttrSet("dfcloud_network.test", "status"),
+ resource.TestCheckResourceAttrSet("dfcloud_network.test", "vpc.resource_id"),
+ resource.TestCheckResourceAttrSet("dfcloud_network.test", "vpc.account_id"),
+ ),
+ },
+ // Import State
+ {
+ ResourceName: "dfcloud_network.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ },
+ })
+}
+
+func testAccNetworkResourceConfig(name string) string {
+ return fmt.Sprintf(`
+resource "dfcloud_network" "test" {
+ name = %[1]q
+
+ location = {
+ provider = "aws"
+ region = "us-east-1"
+ }
+
+ cidr_block = "10.0.0.0/16"
+}
+`, name)
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
new file mode 100644
index 0000000..8b0f495
--- /dev/null
+++ b/internal/provider/provider.go
@@ -0,0 +1,97 @@
+package provider
+
+import (
+ "context"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "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"
+)
+
+type DragonflyDBCloudProvider struct {
+ version string
+}
+
+type ProviderSchema struct {
+ ApiKey types.String `tfsdk:"api_key"`
+ ApiHost types.String `tfsdk:"api_host"`
+}
+
+func NewDragonflyDBCloudProvider(version string) func() provider.Provider {
+ return func() provider.Provider {
+ return &DragonflyDBCloudProvider{
+ version: version,
+ }
+ }
+}
+
+func (p DragonflyDBCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
+ resp.TypeName = "dfcloud_"
+ resp.Version = p.version
+}
+
+func (p DragonflyDBCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "api_key": schema.StringAttribute{
+ Optional: true,
+ Sensitive: true,
+ Description: "Dragonfly Cloud API key. This can also be set via the DFCLOUD_API_KEY environment variable.",
+ },
+ "api_host": schema.StringAttribute{
+ Optional: true,
+ Description: "The URL of the Dragonfly Cloud API.",
+ },
+ },
+ Description: `The Dragonfly Cloud provider is used to interact with resources supported by Dragonfly Cloud.
+
+The provider needs to be configured with the proper credentials before it can be used.`,
+ }
+}
+
+func (p DragonflyDBCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
+ var config ProviderSchema
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var options []dfcloud.ClientOption
+ if config.ApiKey.ValueString() != "" {
+ options = append(options, dfcloud.WithAPIKey(config.ApiKey.ValueString()))
+
+ } else {
+ options = append(options, dfcloud.WithAPIKeyFromEnv())
+ }
+
+ if config.ApiHost.ValueString() != "" {
+ options = append(options, dfcloud.WithAPIHost(config.ApiHost.ValueString()))
+ }
+
+ client, err := dfcloud.NewClient(options...)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create client", err.Error())
+ return
+ }
+
+ resp.ResourceData = client
+}
+
+func (p DragonflyDBCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
+ return []func() datasource.DataSource{
+ // Provider specific implementation
+ }
+}
+
+func (p DragonflyDBCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
+ return []func() resource.Resource{
+ NewDatastoreResource,
+ NewNetworkResource,
+ NewConnectionResource,
+ }
+}
+
+var _ provider.Provider = &DragonflyDBCloudProvider{}
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
new file mode 100644
index 0000000..c1f6c7d
--- /dev/null
+++ b/internal/provider/provider_test.go
@@ -0,0 +1,36 @@
+package provider
+
+import (
+ "os"
+ "testing"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-framework/providerserver"
+ "github.com/hashicorp/terraform-plugin-go/tfprotov6"
+)
+
+var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
+ "dfcloud": providerserver.NewProtocol6WithError(NewDragonflyDBCloudProvider("dev")()),
+}
+
+func testAccPreCheck(t *testing.T) {
+ if os.Getenv("DFCLOUD_API_KEY") == "" {
+ t.Fatalf("DFCLOUD_API_KEY environment variable must be set for acceptance tests")
+ }
+}
+
+var tc *dfcloud.Client
+
+func testClient() *dfcloud.Client {
+ if tc == nil {
+ var options []dfcloud.ClientOption
+
+ options = append(options, dfcloud.WithAPIKeyFromEnv())
+
+ client, _ := dfcloud.NewClient(options...)
+
+ tc = client
+ }
+
+ return tc
+}
diff --git a/internal/resource_model/connection.go b/internal/resource_model/connection.go
new file mode 100644
index 0000000..b6231b5
--- /dev/null
+++ b/internal/resource_model/connection.go
@@ -0,0 +1,77 @@
+package resource_model
+
+import (
+ "context"
+ "time"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type Connection struct {
+ ConnectionID types.String `tfsdk:"connection_id"`
+ Name types.String `tfsdk:"name"`
+ NetworkID types.String `tfsdk:"network_id"`
+ Peer *PeerConfigModel `tfsdk:"peer"`
+ Status types.String `tfsdk:"status"`
+ StatusDetail types.String `tfsdk:"status_detail"`
+ PeerConnID types.String `tfsdk:"peer_connection_id"`
+}
+
+type PeerConfigModel struct {
+ AccountID types.String `tfsdk:"account_id"`
+ VPCID types.String `tfsdk:"vpc_id"`
+ Region types.String `tfsdk:"region"`
+}
+
+func IntoPeerConfig(in PeerConfigModel) dfcloud.PeerConfig {
+ return dfcloud.PeerConfig{
+ AccountID: in.AccountID.ValueString(),
+ VPCID: in.VPCID.ValueString(),
+ Region: in.Region.ValueString(),
+ }
+}
+
+func IntoConnectionConfig(in Connection) *dfcloud.ConnectionConfig {
+ return &dfcloud.ConnectionConfig{
+ Name: in.Name.ValueString(),
+ NetworkID: in.NetworkID.ValueString(),
+ Peer: IntoPeerConfig(*in.Peer),
+ }
+}
+
+func FromConnectionConfig(in *dfcloud.Connection) *Connection {
+ return &Connection{
+ ConnectionID: types.StringValue(in.ID),
+ Name: types.StringValue(in.Config.Name),
+ NetworkID: types.StringValue(in.Config.NetworkID),
+ Peer: &PeerConfigModel{
+ AccountID: types.StringValue(in.Config.Peer.AccountID),
+ VPCID: types.StringValue(in.Config.Peer.VPCID),
+ Region: types.StringValue(in.Config.Peer.Region),
+ },
+ Status: types.StringValue(string(in.Status)),
+ StatusDetail: types.StringValue(in.StatusDetail),
+ PeerConnID: types.StringValue(in.PeerConnectionID),
+ }
+}
+
+func WaitUntilConnectionStatus(ctx context.Context, client *dfcloud.Client, id string, status dfcloud.ConnectionStatus) (*dfcloud.Connection, error) {
+ for {
+ conn, err := client.GetConnection(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ if conn.Status == status {
+ return conn, nil
+ }
+
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(5 * time.Second):
+
+ }
+ }
+}
diff --git a/internal/resource_model/datastore.go b/internal/resource_model/datastore.go
new file mode 100644
index 0000000..af8ac13
--- /dev/null
+++ b/internal/resource_model/datastore.go
@@ -0,0 +1,158 @@
+package resource_model
+
+import (
+ "context"
+ "time"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/samber/lo"
+)
+
+// Datastore maps the resource schema data.
+type Datastore struct {
+ ID types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ NetworkId types.String `tfsdk:"network_id"`
+ Location DatastoreLocation `tfsdk:"location"`
+ Tier DatastoreTier `tfsdk:"tier"`
+ Dragonfly types.Object `tfsdk:"dragonfly"`
+ CreatedAt types.Int64 `tfsdk:"created_at"`
+ Password types.String `tfsdk:"password"`
+ Addr types.String `tfsdk:"addr"`
+}
+
+type DatastoreLocation struct {
+ Provider types.String `tfsdk:"provider"`
+ Region types.String `tfsdk:"region"`
+ AvailabilityZones types.List `tfsdk:"availability_zones"`
+}
+
+type DatastoreTier struct {
+ Memory types.Int64 `tfsdk:"max_memory_bytes"`
+ PerformanceTier types.String `tfsdk:"performance_tier"`
+ Replicas types.Int64 `tfsdk:"replicas"`
+}
+
+func (d *Datastore) FromConfig(in *dfcloud.Datastore) {
+ d.ID = types.StringValue(in.ID)
+ d.Name = types.StringValue(in.Config.Name)
+ d.NetworkId = types.StringNull()
+ d.CreatedAt = types.Int64Value(in.CreatedAt)
+ d.Location.Provider = types.StringValue(string(in.Config.Location.Provider))
+ d.Location.Region = types.StringValue(in.Config.Location.Region)
+ d.Location.AvailabilityZones, _ = types.ListValueFrom(context.Background(), types.StringType, in.Config.Location.AvailabilityZones)
+ d.Addr = types.StringValue(in.Addr)
+ d.Password = types.StringValue(in.Key)
+ d.Tier.Memory = types.Int64Value(int64(in.Config.Tier.Memory))
+ d.Tier.PerformanceTier = types.StringValue(string(in.Config.Tier.PerformanceTier))
+ d.Tier.Replicas = types.Int64Value(int64(*in.Config.Tier.Replicas))
+
+ aclRules, _ := types.ListValueFrom(context.Background(), types.StringType, in.Config.Dragonfly.AclRules)
+ d.Dragonfly = types.ObjectValueMust(map[string]attr.Type{
+ "cache_mode": types.BoolType,
+ "tls": types.BoolType,
+ "bullmq": types.BoolType,
+ "sidekiq": types.BoolType,
+ "memcached": types.BoolType,
+ "acl_rules": types.ListType{ElemType: types.StringType},
+ }, map[string]attr.Value{
+ "cache_mode": types.BoolPointerValue(in.Config.Dragonfly.CacheMode),
+ "tls": types.BoolPointerValue(in.Config.Dragonfly.TLS),
+ "bullmq": types.BoolPointerValue(in.Config.Dragonfly.BullMQ),
+ "sidekiq": types.BoolPointerValue(in.Config.Dragonfly.Sidekiq),
+ "memcached": types.BoolPointerValue(in.Config.Dragonfly.Memcached),
+ "acl_rules": aclRules,
+ })
+
+ if in.Config.NetworkID != "" {
+ d.NetworkId = types.StringValue(in.Config.NetworkID)
+ }
+
+}
+
+func IntoDatastoreConfig(in Datastore) *dfcloud.Datastore {
+ var zones []string
+ for _, z := range in.Location.AvailabilityZones.Elements() {
+ zones = append(zones, z.String())
+ }
+
+ datastore := &dfcloud.Datastore{
+ ID: in.ID.ValueString(),
+ Config: dfcloud.DatastoreConfig{
+ Name: in.Name.ValueString(),
+ Location: dfcloud.DatastoreLocation{
+ Provider: dfcloud.CloudProvider(in.Location.Provider.ValueString()),
+ Region: in.Location.Region.ValueString(),
+ AvailabilityZones: zones,
+ },
+ Tier: dfcloud.DatastoreTier{
+ Memory: uint64(in.Tier.Memory.ValueInt64()),
+ PerformanceTier: dfcloud.PerformanceTier(in.Tier.PerformanceTier.ValueString()),
+ Replicas: lo.ToPtr(int(in.Tier.Replicas.ValueInt64())),
+ },
+ },
+ }
+
+ if !in.NetworkId.IsNull() {
+ datastore.Config.NetworkID = in.NetworkId.ValueString()
+ }
+
+ if in.Dragonfly.IsNull() {
+ in.Dragonfly = types.ObjectValueMust(map[string]attr.Type{
+ "cache_mode": types.BoolType,
+ "tls": types.BoolType,
+ "bullmq": types.BoolType,
+ "sidekiq": types.BoolType,
+ "memcached": types.BoolType,
+ "acl_rules": types.ListType{ElemType: types.StringType},
+ }, map[string]attr.Value{})
+ }
+
+ if in.Dragonfly.Attributes()["cache_mode"] != nil {
+ datastore.Config.Dragonfly.CacheMode = lo.ToPtr(in.Dragonfly.Attributes()["cache_mode"].(types.Bool).ValueBool())
+ }
+ if in.Dragonfly.Attributes()["tls"] != nil {
+ datastore.Config.Dragonfly.TLS = lo.ToPtr(in.Dragonfly.Attributes()["tls"].(types.Bool).ValueBool())
+ }
+ if in.Dragonfly.Attributes()["bullmq"] != nil {
+ datastore.Config.Dragonfly.BullMQ = lo.ToPtr(in.Dragonfly.Attributes()["bullmq"].(types.Bool).ValueBool())
+ }
+ if in.Dragonfly.Attributes()["sidekiq"] != nil {
+ datastore.Config.Dragonfly.Sidekiq = lo.ToPtr(in.Dragonfly.Attributes()["sidekiq"].(types.Bool).ValueBool())
+ }
+ if in.Dragonfly.Attributes()["memcached"] != nil {
+ datastore.Config.Dragonfly.Memcached = lo.ToPtr(in.Dragonfly.Attributes()["memcached"].(types.Bool).ValueBool())
+ }
+
+ if in.Dragonfly.Attributes()["acl_rules"] != nil {
+ var rules dfcloud.AclRuleArray
+ for _, rule := range in.Dragonfly.Attributes()["acl_rules"].(types.List).Elements() {
+ rules = append(rules, rule.String())
+ }
+ datastore.Config.Dragonfly.AclRules = &rules
+ }
+
+ return datastore
+}
+
+// Helper function to wait for datastore to become active.
+func WaitForDatastoreStatus(ctx context.Context, client *dfcloud.Client, id string, status dfcloud.DatastoreStatus) (*dfcloud.Datastore, error) {
+ for {
+ datastore, err := client.GetDatastore(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ if datastore.Status == status {
+ return datastore, nil
+ }
+
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(5 * time.Second):
+ }
+ }
+}
diff --git a/internal/resource_model/network.go b/internal/resource_model/network.go
new file mode 100644
index 0000000..1d39cc1
--- /dev/null
+++ b/internal/resource_model/network.go
@@ -0,0 +1,79 @@
+package resource_model
+
+import (
+ "context"
+ "time"
+
+ dfcloud "github.com/dragonflydb/terraform-provider-dfcloud/internal/sdk"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type NetworkLocation struct {
+ Provider types.String `tfsdk:"provider"`
+ Region types.String `tfsdk:"region"`
+}
+
+type Network struct {
+ Id types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Location *NetworkLocation `tfsdk:"location"`
+ CidrBlock types.String `tfsdk:"cidr_block"`
+ CreatedAt types.Int64 `tfsdk:"created_at"`
+ Status types.String `tfsdk:"status"`
+ Vpc types.Object `tfsdk:"vpc"`
+}
+
+func IntoNetworkConfig(in Network) *dfcloud.NetworkConfig {
+ return &dfcloud.NetworkConfig{
+ Name: in.Name.ValueString(),
+ Location: dfcloud.NetworkLocation{
+ Provider: dfcloud.CloudProvider(in.Location.Provider.ValueString()),
+ Region: in.Location.Region.ValueString(),
+ },
+ CIDRBlock: in.CidrBlock.ValueString(),
+ }
+}
+
+func FromNetworkConfig(in *dfcloud.Network) *Network {
+ return &Network{
+ Id: types.StringValue(in.ID),
+ Name: types.StringValue(in.Name),
+ Location: &NetworkLocation{
+ Provider: types.StringValue(string(in.Location.Provider)),
+ Region: types.StringValue(in.Location.Region),
+ },
+ CidrBlock: types.StringValue(in.CIDRBlock),
+ CreatedAt: types.Int64Value(in.CreatedAt),
+ Status: types.StringValue(string(in.Status)),
+ Vpc: types.ObjectValueMust(
+ map[string]attr.Type{
+ "resource_id": types.StringType,
+ "account_id": types.StringType,
+ },
+ map[string]attr.Value{
+ "resource_id": types.StringValue(in.VPC.ResourceID),
+ "account_id": types.StringValue(in.VPC.AccountID),
+ },
+ ),
+ }
+}
+
+func WaitUntilNetworkStatus(ctx context.Context, client *dfcloud.Client, id string, status dfcloud.NetworkStatus) (*dfcloud.Network, error) {
+ for {
+ network, err := client.GetNetwork(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ if network.Status == status {
+ return network, nil
+ }
+
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(5 * time.Second):
+ }
+ }
+}
diff --git a/internal/sdk/client.go b/internal/sdk/client.go
new file mode 100644
index 0000000..6c7aaa8
--- /dev/null
+++ b/internal/sdk/client.go
@@ -0,0 +1,336 @@
+package sdk
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+)
+
+type errorResponse struct {
+ Error string `json:"error"`
+}
+
+type clientOptions struct {
+ apiKey string
+ apiHost string
+ timeout time.Duration
+}
+
+type ClientOption interface {
+ apply(*clientOptions)
+}
+
+type apiKeyOption string
+
+func (o apiKeyOption) apply(opts *clientOptions) {
+ opts.apiKey = string(o)
+}
+
+// WithAPIKey configures the client to authenticate with Dragonfly cloud using
+// the given API key.
+//
+// You can get an API key from the Dragonfly cloud dashboard.
+func WithAPIKey(key string) ClientOption {
+ return apiKeyOption(key)
+}
+
+// WithAPIKeyFromEnv is a shortcut for calling [WithAPIKey] with the
+// value of the DFCLOUD_API_KEY environment variable.
+func WithAPIKeyFromEnv() ClientOption {
+ return WithAPIKey(os.Getenv("DFCLOUD_API_KEY"))
+}
+
+type timeoutOption time.Duration
+
+func (o timeoutOption) apply(opts *clientOptions) {
+ opts.timeout = time.Duration(o)
+}
+
+// WithTimeout configures the client request timeout.
+func WithTimeout(timeout time.Duration) ClientOption {
+ return timeoutOption(timeout)
+}
+
+type apiHostOption string
+
+func (o apiHostOption) apply(opts *clientOptions) {
+ opts.apiHost = string(o)
+}
+
+// WithAPIHost configures the client to use the given API URL.
+func WithAPIHost(url string) ClientOption {
+ return apiHostOption(url)
+}
+
+// Client represents a REST client for the Dragonfly cloud API.
+type Client struct {
+ apiKey string
+ apiHost string
+
+ httpClient *http.Client
+}
+
+// NewClient creates a Dragonfly cloud client.
+//
+// The client options must include either [WithAPIKey] or [WithAPIKeyFromEnv]
+// to authenticate with Dragonfly cloud.
+func NewClient(opts ...ClientOption) (*Client, error) {
+ options := clientOptions{
+ timeout: time.Second * 15,
+ }
+ for _, o := range opts {
+ o.apply(&options)
+ }
+
+ if options.apiKey == "" {
+ return nil, fmt.Errorf("missing api key")
+ }
+
+ if options.apiHost == "" {
+ // use default
+ options.apiHost = "api.dragonflydb.cloud"
+ }
+
+ return &Client{
+ apiKey: options.apiKey,
+ httpClient: &http.Client{
+ Timeout: options.timeout,
+ },
+ apiHost: options.apiHost,
+ }, nil
+}
+
+func (c *Client) GetDatastore(ctx context.Context, id string) (*Datastore, error) {
+ r, err := c.request(ctx, http.MethodGet, "/v1/datastores/"+id, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var datastore *Datastore
+ if err := json.NewDecoder(r).Decode(&datastore); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return datastore, nil
+}
+
+func (c *Client) CreateDatastore(ctx context.Context, config *DatastoreConfig) (*Datastore, error) {
+ b, _ := json.Marshal(&config)
+
+ r, err := c.request(ctx, http.MethodPost, "/v1/datastores", b)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var datastore Datastore
+ if err := json.NewDecoder(r).Decode(&datastore); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return &datastore, nil
+}
+
+func (c *Client) UpdateDatastore(ctx context.Context, id string, config *DatastoreConfig) (*Datastore, error) {
+ b, _ := json.Marshal(&config)
+
+ r, err := c.request(ctx, http.MethodPut, "/v1/datastores/"+id, b)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var datastore Datastore
+ if err := json.NewDecoder(r).Decode(&datastore); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return &datastore, nil
+}
+
+// ListDatastores lists all the customers datastores.
+func (c *Client) ListDatastores(ctx context.Context) ([]*Datastore, error) {
+ r, err := c.request(ctx, http.MethodGet, "/v1/datastores", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var datastores []*Datastore
+ if err := json.NewDecoder(r).Decode(&datastores); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+
+ return datastores, nil
+}
+
+func (c *Client) DeleteDatastore(ctx context.Context, id string) error {
+ r, err := c.request(ctx, http.MethodDelete, "/v1/datastores/"+id, nil)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ return nil
+}
+
+func (c *Client) GetNetwork(ctx context.Context, id string) (*Network, error) {
+ r, err := c.request(ctx, http.MethodGet, "/v1/networks/"+id, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var network *Network
+ if err := json.NewDecoder(r).Decode(&network); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return network, nil
+}
+
+func (c *Client) CreateNetwork(ctx context.Context, config *NetworkConfig) (*Network, error) {
+ b, _ := json.Marshal(&config)
+
+ r, err := c.request(ctx, http.MethodPost, "/v1/networks", b)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var network Network
+ if err := json.NewDecoder(r).Decode(&network); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return &network, nil
+}
+
+// ListNetworks lists all the customers networks.
+func (c *Client) ListNetworks(ctx context.Context) ([]*Network, error) {
+ r, err := c.request(ctx, http.MethodGet, "/v1/networks", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var networks []*Network
+ if err := json.NewDecoder(r).Decode(&networks); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+
+ return networks, nil
+}
+
+func (c *Client) DeleteNetwork(ctx context.Context, id string) error {
+ r, err := c.request(ctx, http.MethodDelete, "/v1/networks/"+id, nil)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ return nil
+}
+
+func (c *Client) GetConnection(ctx context.Context, id string) (*Connection, error) {
+ r, err := c.request(ctx, http.MethodGet, "/v1/connections/"+id, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var conn *Connection
+ if err := json.NewDecoder(r).Decode(&conn); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return conn, nil
+}
+
+func (c *Client) CreateConnection(ctx context.Context, config *ConnectionConfig) (*Connection, error) {
+ b, _ := json.Marshal(&config)
+
+ r, err := c.request(ctx, http.MethodPost, "/v1/connections", b)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var conn Connection
+ if err := json.NewDecoder(r).Decode(&conn); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ return &conn, nil
+}
+
+// ListConnection lists all the customers connections.
+func (c *Client) ListConnections(ctx context.Context) ([]*Connection, error) {
+ r, err := c.request(ctx, http.MethodGet, "/v1/connections", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var conns []*Connection
+ if err := json.NewDecoder(r).Decode(&conns); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+
+ return conns, nil
+}
+
+func (c *Client) DeleteConnection(ctx context.Context, id string) error {
+ r, err := c.request(ctx, http.MethodDelete, "/v1/connections/"+id, nil)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ return nil
+}
+
+func (c *Client) request(
+ ctx context.Context,
+ method string,
+ path string,
+ body []byte,
+) (io.ReadCloser, error) {
+ url := &url.URL{
+ Scheme: "https",
+ Host: c.apiHost,
+ Path: path,
+ }
+
+ var b io.Reader
+ if body != nil {
+ b = bytes.NewReader(body)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), b)
+ if err != nil {
+ return nil, fmt.Errorf("request: %w", err)
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= http.StatusBadRequest &&
+ resp.StatusCode < http.StatusInternalServerError {
+ var errResp errorResponse
+ if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
+ return nil, fmt.Errorf("bad status: %d", resp.StatusCode)
+ }
+ return nil, fmt.Errorf("bad status: %d: %s", resp.StatusCode, errResp.Error)
+ }
+ return nil, fmt.Errorf("bad status: %d", resp.StatusCode)
+ }
+
+ return resp.Body, nil
+}
diff --git a/internal/sdk/connection.go b/internal/sdk/connection.go
new file mode 100644
index 0000000..0a081c6
--- /dev/null
+++ b/internal/sdk/connection.go
@@ -0,0 +1,61 @@
+package sdk
+
+// NetworkStatus represents the current status of the connection.
+type ConnectionStatus string
+
+const (
+ // ConnectionStatusPending is set when the user has requested and it is
+ // being asynchronously setup.
+ ConnectionStatusPending ConnectionStatus = "pending"
+ // ConnectionStatusActive indicates the peer connection has been
+ // established.
+ ConnectionStatusActive ConnectionStatus = "active"
+ // ConnectionStatusInactive indicates the peer connection has not yet been
+ // approved on the customers account.
+ ConnectionStatusInactive ConnectionStatus = "inactive"
+ // ConnectionStatusIrrecoverable indicates the peer connection was deleted
+ // from the customers account.
+ ConnectionStatusIrrecoverable ConnectionStatus = "irrecoverable"
+ // ConnectionStatusDeleting is set when the user has requested the
+ // connection to be deleted and it is being asynchronously deprovisioned.
+ ConnectionStatusDeleting ConnectionStatus = "deleting"
+ // ConnectionStatusDeleting is set when the network has been deprovisioned.
+ ConnectionStatusDeleted ConnectionStatus = "deleted"
+ // ConnectionStatusFailed indicates the peer connection was requested but
+ // could not be connected.
+ ConnectionStatusFailed ConnectionStatus = "failed"
+)
+
+// PeerConfig describes the VPC to connect to.
+type PeerConfig struct {
+ // AccountID is the account ID of the target VPC.
+ AccountID string `json:"account_id"`
+ // CIDRBlock is the CIDR block of the target VPC.
+ CIDRBlock string `json:"cidr_block"`
+ // VPCID is the ID of the target VPC.
+ VPCID string `json:"vpc_id"`
+ // Region is the region of the target VPC. Only specify if the target VPC
+ // is in a different region to the network your connecting to.
+ Region string `json:"region,omitempty"`
+}
+
+type ConnectionConfig struct {
+ Name string `json:"name"`
+ NetworkID string `json:"network_id"`
+ Peer PeerConfig `json:"peer"`
+}
+
+// Connection represents a network peer-connection.
+type Connection struct {
+ ID string `json:"connection_id"`
+
+ Status ConnectionStatus `json:"status"`
+
+ // StatusDetail provides more information on the status of the connection.
+ StatusDetail string `json:"status_detail,omitempty"`
+
+ // PeerConnectionID is the underlying cloud provider peer connection ID.
+ PeerConnectionID string `json:"peer_connection_id"`
+
+ Config *ConnectionConfig `json:"connection_config"`
+}
diff --git a/internal/sdk/datastore.go b/internal/sdk/datastore.go
new file mode 100644
index 0000000..b445d78
--- /dev/null
+++ b/internal/sdk/datastore.go
@@ -0,0 +1,145 @@
+package sdk
+
+type CloudProvider string
+
+const (
+ CloudProviderAWS CloudProvider = "aws"
+ CloudProviderGCP CloudProvider = "gcp"
+ CloudProviderAzure CloudProvider = "azure"
+)
+
+
+// DatastoreLocation represents where the datastore should be provisioned.
+type DatastoreLocation struct {
+ Provider CloudProvider `json:"provider"`
+ Region string `json:"region"`
+ // AvailabilityZones indicates which availability zones the datastore
+ // should use in priority order.
+ AvailabilityZones []string `json:"availability_zones"`
+}
+
+type PerformanceTier string
+type AclRuleArray []string
+
+const (
+ PerformanceTierDev PerformanceTier = "dev"
+ PerformanceTierStandard PerformanceTier = "standard"
+ PerformanceTierEnhanced PerformanceTier = "enhanced"
+)
+
+var PerformanceTiers = []PerformanceTier{
+ PerformanceTierDev,
+ PerformanceTierStandard,
+ PerformanceTierEnhanced,
+}
+
+func PerformanceTiersString() []string {
+ var ss []string
+ for _, tier := range PerformanceTiers {
+ ss = append(ss, string(tier))
+ }
+ return ss
+}
+
+type DatastoreTier struct {
+ // Memory is the maximum number of bytes Dragonfly can consume.
+ Memory uint64 `json:"max_memory_bytes"`
+ // PerformanceTier determines number of CPUs provisioned relative to
+ // memory.
+ PerformanceTier PerformanceTier `json:"performance_tier"`
+
+ // Replicas is the number of Dragonfly replicas (not including the master).
+ Replicas *int `json:"replicas"`
+}
+
+type DatastoreDragonflyConfig struct {
+ CacheMode *bool `json:"cache_mode"`
+ TLS *bool `json:"tls"`
+ BullMQ *bool `json:"bullmq"`
+ Sidekiq *bool `json:"sidekiq"`
+ Memcached *bool `json:"memcached"`
+ AclRules *AclRuleArray `json:"acl_rules"`
+}
+
+// DatastoreConfig contains the datastores configurable fields.
+type DatastoreConfig struct {
+ Name string `json:"name"`
+ // NetworkID is an optional ID of a dedicated network to provision the
+ // datastore in.
+ NetworkID string `json:"network_id"`
+ Location DatastoreLocation `json:"location"`
+ Tier DatastoreTier `json:"tier"`
+ // Dragonfly contains the Dragonfly node configuration.
+ Dragonfly DatastoreDragonflyConfig `json:"dragonfly"`
+
+ BackupPolicy BackupPolicy `json:"backup_policy" mapstructure:"backup_policy"`
+
+ Restore RestoreBackup `json:"restore"`
+
+ DisablePasskey bool `json:"disable_passkey"`
+}
+
+type RestoreBackup struct {
+ // Backup contains the ID of the backup to restore.
+ BackupId string `json:"backup_id"`
+ // Loaded denotes if the backup is loaded onto the datastore
+ Loaded bool `json:"loaded"`
+}
+
+type BackupPolicy struct {
+ Enabled *bool `json:"enabled"`
+ Retention int `json:"retention,omitempty"`
+ EveryHour *bool `json:"every_hour,omitempty"`
+ EveryDay *bool `json:"every_day,omitempty"`
+ Hours []int `json:"hours,omitempty"`
+ WeekDays []int `json:"weekdays,omitempty"`
+}
+
+type DatastoreDashboard struct {
+ // URL contains the datastores public Grafana dashboard URL.
+ URL string `json:"url"`
+}
+
+// DatastoreStatus represents the current status of the datastore.
+type DatastoreStatus string
+
+const (
+ // DatastoreStatusPending is set when the user has requested the datastore
+ // and it is being asynchronously provisioned.
+ DatastoreStatusPending DatastoreStatus = "pending"
+ // DatastoreStatusUpdating is set when a user has requested an update and
+ // it is being asynchronously provisioned.
+ DatastoreStatusUpdating DatastoreStatus = "updating"
+ // DatastoreStatusRestoring is set when a user has requested a backup
+ // that is being asyncronously restored.
+ DatastoreStatusRestoring DatastoreStatus = "restoring"
+ // DatastoreStatusActive is set when the datastore has been provisioned and
+ // is usable.
+ DatastoreStatusActive DatastoreStatus = "active"
+ // DatastoreStatusDeleting is set when the user has requested the datastore
+ // to be deleted and it is being asynchronously deprovisioned.
+ DatastoreStatusDeleting DatastoreStatus = "deleting"
+ // DatastoreStatusDeleted is set when the datastore has been deprovisioned.
+ DatastoreStatusDeleted DatastoreStatus = "deleted"
+)
+
+type Datastore struct {
+ // ID is a unique identifier for the datastore.
+ ID string `json:"datastore_id"`
+
+ Status DatastoreStatus `json:"status"`
+
+ CreatedAt int64 `json:"created_at" mapstructure:"created_at"`
+
+ // Key is the Dragonfly key to configure when connecting to your
+ // datastore.
+ Key string `json:"password"`
+
+ // Addr is the hostname and port of your datastore.
+ Addr string `json:"addr"`
+
+ // Dashboard contains details on the datastores public Grafana dashboard.
+ Dashboard *DatastoreDashboard `json:"dashboard"`
+
+ Config DatastoreConfig `json:"config"`
+}
diff --git a/internal/sdk/network.go b/internal/sdk/network.go
new file mode 100644
index 0000000..570978a
--- /dev/null
+++ b/internal/sdk/network.go
@@ -0,0 +1,54 @@
+package sdk
+
+// NetworkStatus represents the current status of the network.
+type NetworkStatus string
+
+const (
+ // NetworkStatusPending is set when the user has requested the network
+ // and it is being asynchronously provisioned.
+ NetworkStatusPending NetworkStatus = "pending"
+ // DatastoreStatusActive is set when the network has been provisioned.
+ NetworkStatusActive NetworkStatus = "active"
+ // DatastoreStatusActive is set when the network was requested but could
+ // not be provisioned.
+ NetworkStatusFailed NetworkStatus = "failed"
+ // NetworkStatusDeleting is set when the user has requested the network to
+ // be deleted and it is being asynchronously deprovisioned.
+ NetworkStatusDeleting NetworkStatus = "deleting"
+ // NetworkStatusDeleted is set when the network has been deprovisioned.
+ NetworkStatusDeleted NetworkStatus = "deleted"
+)
+
+// NetworkLocation represents where the network should be provisioned.
+type NetworkLocation struct {
+ Provider CloudProvider `json:"provider"`
+ Region string `json:"region"`
+}
+
+type NetworkVPC struct {
+ // ResourceID is the ID of the VPC.
+ ResourceID string `json:"resource_id"`
+ // AccountID is the Dragonfly Cloud account ID that owns the VPC resource.
+ // This is required to setup peering connections.
+ AccountID string `json:"account_id"`
+}
+
+type NetworkConfig struct {
+ Name string `json:"name"`
+ Location NetworkLocation `json:"location"`
+ CIDRBlock string `json:"cidr_block"`
+}
+
+type Network struct {
+ ID string `json:"network_id"`
+
+ Status NetworkStatus `json:"status"`
+
+ CreatedAt int64 `json:"created_at"`
+
+ // VPC contains details on the networks provisioned VPC. This is required
+ // to setup VPC peering.
+ VPC *NetworkVPC `json:"vpc"`
+
+ *NetworkConfig
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..a0ad7f7
--- /dev/null
+++ b/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "log"
+
+ "github.com/dragonflydb/terraform-provider-dfcloud/internal/provider"
+ "github.com/hashicorp/terraform-plugin-framework/providerserver"
+)
+
+var (
+ // version is the version of the provider.
+ // This is set at compile time using -ldflags.
+ version string = "dev"
+)
+
+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/dragonflydb/dfcloud",
+ Debug: debug,
+ }
+
+ err := providerserver.Serve(context.Background(), provider.NewDragonflyDBCloudProvider(version), opts)
+ if err != nil {
+ log.Fatal(err.Error())
+ }
+}
diff --git a/terraform-registry-manifest.json b/terraform-registry-manifest.json
new file mode 100644
index 0000000..6308a03
--- /dev/null
+++ b/terraform-registry-manifest.json
@@ -0,0 +1,8 @@
+{
+ "version": 1,
+ "metadata": {
+ "protocol_versions": [
+ "6.0"
+ ]
+ }
+}