Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
joescharf committed Jun 17, 2024
0 parents commit 8c65bd7
Showing 35 changed files with 2,864 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .copywrite.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# NOTE: This file is for HashiCorp specific licensing automation and can be deleted after creating a new repo with this template.
schema_version = 1

project {
license = "MPL-2.0"
copyright_year = 2021

header_ignore = [
# examples used within documentation (prose)
"examples/**",

# GitHub issue template configuration
".github/ISSUE_TEMPLATE/*.yml",

# golangci-lint tooling configuration
".golangci.yml",

# GoReleaser tooling configuration
".goreleaser.yml",
]
}
5 changes: 5 additions & 0 deletions .github/CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Code of Conduct

HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code.

Please read the full text at https://www.hashicorp.com/community-guidelines
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# See GitHub's documentation for more information on this file:
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
# TODO: Dependabot only updates hashicorp GHAs in the template repository, the following lines can be removed for consumers of this template
allow:
- dependency-name: "hashicorp/*"
21 changes: 21 additions & 0 deletions .github/workflows/issue-comment-triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# DO NOT EDIT - This GitHub Workflow is managed by automation
# https://github.com/hashicorp/terraform-devex-repos
name: Issue Comment Triage

on:
issue_comment:
types: [created]

jobs:
issue_comment_triage:
runs-on: ubuntu-latest
env:
# issue_comment events are triggered by comments on issues and pull requests. Checking the
# value of github.event.issue.pull_request tells us whether the issue is an issue or is
# actually a pull request, allowing us to dynamically set the gh subcommand:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only
COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }}
GH_TOKEN: ${{ github.token }}
steps:
- name: 'Remove waiting-response on comment'
run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response
27 changes: 27 additions & 0 deletions .github/workflows/lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# DO NOT EDIT - This GitHub Workflow is managed by automation
# https://github.com/hashicorp/terraform-devex-repos
name: 'Lock Threads'

on:
schedule:
- cron: '43 20 * * *'

jobs:
lock:
runs-on: ubuntu-latest
steps:
# NOTE: When TSCCR updates the GitHub action version, update the template workflow file to avoid drift:
# https://github.com/hashicorp/terraform-devex-repos/blob/main/modules/repo/workflows/lock.tftpl
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
github-token: ${{ github.token }}
issue-comment: >
I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
issue-inactive-days: '30'
pr-comment: >
I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
pr-inactive-days: '30'
57 changes: 57 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Terraform Provider release workflow.
name: Release

# This GitHub action creates a release when a tag that matches the pattern
# "v*" (e.g. v0.1.0) is created.
on:
push:
tags:
- 'v*'

# Releases need permissions to read and write the repository contents.
# GitHub considers creating releases and uploading assets as writing contents.
permissions:
contents: write
env:
GOPRIVATE: github.com/joescharf/*

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Add SSH Go Module Private Key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.DEPLOY_KEY }}"
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
- name: Setup access for private go modules
run: |
git config --global url."ssh://git@github.com/joescharf/dbsnapper-cli.git".insteadOf https://github.com/joescharf/dbsnapper
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
# Allow goreleaser to access older tag information.
fetch-depth: 0
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache: true
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
id: import_gpg
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@5742e2a039330cbb23ebf35f046f814d4c6ff811 # v5.1.0
with:
args: release --clean
env:
# GitHub sets the GITHUB_TOKEN secret automatically.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
136 changes: 136 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Terraform Provider testing workflow.
# https://aran.dev/posts/github-actions-go-private-modules/
# https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys#deploy-keys

name: Tests

# This GitHub action runs your tests for each pull request and push.
# Optionally, you can turn it on using a schedule for regular testing.
on:
pull_request:
paths-ignore:
- 'README.md'
push:
paths-ignore:
- 'README.md'

# Testing only needs permissions to read the repository contents.
permissions:
contents: write
env:
GOPRIVATE: github.com/joescharf/*


jobs:
# Ensure project builds before running testing matrix
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Add SSH Go Module Private Key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.DEPLOY_KEY }}"
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
- name: Setup access for private go modules
run: |
git config --global url."ssh://git@github.com/joescharf/dbsnapper-cli.git".insteadOf https://github.com/joescharf/dbsnapper
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache: true
- run: go mod download
- run: go build -v .
- name: Run linters
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
with:
version: latest

generate:
runs-on: ubuntu-latest
steps:
- name: Add SSH Go Module Private Key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.DEPLOY_KEY }}"
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
- name: Setup access for private go modules
run: |
git config --global url."ssh://git@github.com/joescharf/dbsnapper-cli.git".insteadOf https://github.com/joescharf/dbsnapper
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache: true
# Temporarily download Terraform 1.8 prerelease for function documentation support.
# When Terraform 1.8.0 final is released, this can be removed.
- uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1
with:
terraform_version: '1.8.0-alpha20240216'
terraform_wrapper: false
- run: go generate ./...
- name: git diff
run: |
git diff --compact-summary --exit-code || \
(echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1)
# Run acceptance tests in a matrix with Terraform CLI versions
# Max parallel = 1 otherwise the tests will fail due to state conflicts.
test:
name: Terraform Provider Acceptance Tests
needs: build
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
max-parallel: 1
matrix:
# list whatever Terraform versions here you would like to support
terraform:
- '1.6.*'
- '1.7.*'
- '1.8.*'
steps:
- name: Add SSH Go Module Private Key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.DEPLOY_KEY }}"
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
- name: Setup access for private go modules
run: |
git config --global url."ssh://git@github.com/joescharf/dbsnapper-cli.git".insteadOf https://github.com/joescharf/dbsnapper
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache: true
- uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1
with:
terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false
- run: go mod download
- env:
TF_ACC: "1"
DBSNAPPER_AUTHTOKEN: ${{ secrets.DBSNAPPER_AUTHTOKEN }}
run: go test -v -cover ./internal/provider/
timeout-minutes: 10
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
*.dll
*.exe
.DS_Store
example.tf
terraform.tfplan
terraform.tfstate
bin/
dist/
modules-dev/
/pkg/
website/.vagrant
website/.bundle
website/build
website/node_modules
.vagrant/
*.backup
./*.tfstate
.terraform/
*.log
*.bak
*~
.*.swp
.idea
*.iml
*.test
*.iml

website/vendor

# Test exclusions
!command/test-fixtures/**/*.tfstate
!command/test-fixtures/**/.terraform/

# Keep windows files with windows line endings
*.winfile eol=crlf
27 changes: 27 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Visit https://golangci-lint.run/ for usage documentation
# and information on other useful linters
issues:
max-per-linter: 0
max-same-issues: 0

linters:
disable-all: true
enable:
- durationcheck
- errcheck
- exportloopref
- forcetypeassert
- godot
- gofmt
- gosimple
- ineffassign
- makezero
- misspell
- nilerr
- predeclared
- staticcheck
- tenv
- unconvert
- unparam
- unused
- vet
60 changes: 60 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Visit https://goreleaser.com for documentation on how to customize this
# behavior.
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:
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:
skip: true
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 0.1.0 (Initial Release)

- Initial provider implementation

FEATURES:

Data sources:

- `targets` - Returns a list of targets

Resources:

- `target` - Target definition management
6 changes: 6 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
default: testacc

# Run acceptance tests
.PHONY: testacc
testacc:
TF_ACC=1 go test ./... -v $(DBSNAPPER_AUTHTOKEN) $(DBSNAPPER_BASE_URL) -timeout 120m
375 changes: 375 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Terraform Provider Scaffolding (Terraform Plugin Framework)

_This template repository is built on the [Terraform Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework). The template repository built on the [Terraform Plugin SDK](https://github.com/hashicorp/terraform-plugin-sdk) can be found at [terraform-provider-scaffolding](https://github.com/hashicorp/terraform-provider-scaffolding). See [Which SDK Should I Use?](https://developer.hashicorp.com/terraform/plugin/framework-benefits) in the Terraform documentation for additional information._

This repository is a *template* for a [Terraform](https://www.terraform.io) provider. It is intended as a starting point for creating Terraform providers, containing:

- A resource and a data source (`internal/provider/`),
- Examples (`examples/`) and generated documentation (`docs/`),
- Miscellaneous meta files.

These files contain boilerplate code that you will need to edit to create your own Terraform provider. Tutorials for creating Terraform providers can be found on the [HashiCorp Developer](https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework) platform. _Terraform Plugin Framework specific guides are titled accordingly._

Please see the [GitHub template repository documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) for how to create a new repository from this template on GitHub.

Once you've written your provider, you'll want to [publish it on the Terraform Registry](https://developer.hashicorp.com/terraform/registry/providers/publishing) so that others can use it.

## Requirements

- [Terraform](https://developer.hashicorp.com/terraform/downloads) >= 1.0
- [Go](https://golang.org/doc/install) >= 1.21

## Building The Provider

1. Clone the repository
1. Enter the repository directory
1. Build the provider using the Go `install` command:

```shell
go install
```

## Adding Dependencies

This provider uses [Go modules](https://github.com/golang/go/wiki/Modules).
Please see the Go documentation for the most up to date information about using Go modules.

To add a new dependency `github.com/author/dependency` to your Terraform provider:

```shell
go get github.com/author/dependency
go mod tidy
```

Then commit the changes to `go.mod` and `go.sum`.

## Using the provider

Fill this in for each provider

## Developing the Provider

If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (see [Requirements](#requirements) above).

To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory.

To generate or update documentation, run `go generate`.

In order to run the full suite of Acceptance tests, run `make testacc`.

*Note:* Acceptance tests create real resources, and often cost money to run.

```shell
make testacc
```
64 changes: 64 additions & 0 deletions docs/data-sources/targets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dbsnapper_targets Data Source - dbsnapper"
subcategory: ""
description: |-
Targets data source
---

# dbsnapper_targets (Data Source)

Targets data source



<!-- schema generated by tfplugindocs -->
## Schema

### Read-Only

- `targets` (Attributes List) (see [below for nested schema](#nestedatt--targets))

<a id="nestedatt--targets"></a>
### Nested Schema for `targets`

Optional:

- `share` (Attributes) The share configuration (see [below for nested schema](#nestedatt--targets--share))

Read-Only:

- `created_at` (String) The time the target was created
- `id` (String) The unique identifier for the target
- `messages` (String) The error messages from the target - determined by agent
- `name` (String) The name of the target
- `sanitize` (Attributes) The sanitize configuration (see [below for nested schema](#nestedatt--targets--sanitize))
- `snapshot` (Attributes) The snapshot configuration (see [below for nested schema](#nestedatt--targets--snapshot))
- `status` (String) The status of the target - determined by agent
- `updated_at` (String) The time the target was last updated

<a id="nestedatt--targets--share"></a>
### Nested Schema for `targets.share`

Optional:

- `sso_groups` (List of String) The SSO groups that have access to the target snapshot


<a id="nestedatt--targets--sanitize"></a>
### Nested Schema for `targets.sanitize`

Read-Only:

- `dst_url` (String) The destination URL of the database used to sanitizea snapshot
- `query` (String) The query used to sanitize the snapshot


<a id="nestedatt--targets--snapshot"></a>
### Nested Schema for `targets.snapshot`

Read-Only:

- `dst_url` (String) The destination URL for the target snapshot (will be overwritten)
- `src_bytes` (Number) The size of the source database in bytes
- `src_url` (String) The source URL for the target snapshot
38 changes: 38 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dbsnapper Provider"
subcategory: ""
description: |-
---

# dbsnapper Provider



## Example Usage

```terraform
terraform {
required_providers {
dbsnapper = {
source = "dbsnapper/dbsnapper"
version = "~>0.1"
}
}
}
provider "dbsnapper" {
# Authentication token for API access
# Omit this if you want to use DBSNAPPER_AUTHTOKEN environment variable
authtoken = var.dbsnapper_authtoken
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `authtoken` (String, Sensitive) DBSnapper API Authtoken
- `base_url` (String) DBSnapper API Base URL - for internal testing only
66 changes: 66 additions & 0 deletions docs/resources/target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dbsnapper_target Resource - dbsnapper"
subcategory: ""
description: |-
Target resource
---

# dbsnapper_target (Resource)

Target resource



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) The name of the target
- `snapshot` (Attributes) The snapshot configuration (see [below for nested schema](#nestedatt--snapshot))

### Optional

- `sanitize` (Attributes) The sanitize configuration (see [below for nested schema](#nestedatt--sanitize))
- `share` (Attributes) The share configuration (see [below for nested schema](#nestedatt--share))

### Read-Only

- `created_at` (String) The time the target was created
- `id` (String) The unique identifier for the target
- `messages` (String) The error messages from the target - determined by agent
- `status` (String) The status of the target - determined by agent
- `updated_at` (String) The time the target was last updated

<a id="nestedatt--snapshot"></a>
### Nested Schema for `snapshot`

Required:

- `src_url` (String) The source URL for the target snapshot

Optional:

- `dst_url` (String) The destination URL for the target snapshot (will be overwritten)

Read-Only:

- `src_bytes` (Number) The size of the source database in bytes


<a id="nestedatt--sanitize"></a>
### Nested Schema for `sanitize`

Optional:

- `dst_url` (String) The destination URL of the database used to sanitizea snapshot
- `query` (String) The query used to sanitize the snapshot


<a id="nestedatt--share"></a>
### Nested Schema for `share`

Optional:

- `sso_groups` (List of String) The SSO groups that have access to the target snapshot
9 changes: 9 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Examples

This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI.

The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation.

* **provider/provider.tf** example file for the provider index page
* **data-sources/`full data source name`/data-source.tf** example file for the named data source page
* **resources/`full resource name`/resource.tf** example file for the named data source page
9 changes: 9 additions & 0 deletions examples/data-sources/targets/data_source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

data "dbsnapper_targets" "example" {}


output "example_targets" {
value = data.dbsnapper_targets.example
}


11 changes: 11 additions & 0 deletions examples/provider-install-verification/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
terraform {
required_providers {
dbsnapper = {
source = "registry.terraform.io/hashicorp/dbsnapper"
}
}
}

provider "dbsnapper" {}

data "dbsnapper_targets" "example" {}
14 changes: 14 additions & 0 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
terraform {
required_providers {
dbsnapper = {
source = "dbsnapper/dbsnapper"
version = "~>0.1"
}
}
}

provider "dbsnapper" {
# Authentication token for API access
# Omit this if you want to use DBSNAPPER_AUTHTOKEN environment variable
authtoken = var.dbsnapper_authtoken
}
23 changes: 23 additions & 0 deletions examples/resources/target/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
resource "dbsnapper_target" "tf_example" {
name = "tf_example"
snapshot = {
src_url = "postgres://user:pass@localhost:5432/tf_example"
dst_url = "postgres://user:pass@localhost:5432/tf_example_snap"
}
sanitize = {
dst_url = "postgres://user:pass@localhost:5432/tf_example_snap_sanitized"
query = <<EOT
DROP TABLE IF EXISTS dbsnapper_info;
CREATE TABLE dbsnapper_info (created_at timestamp, tags text []);
INSERT INTO dbsnapper_info (created_at, tags)
VALUES (NOW(), '{target:tf-example, src:terraform}');
EOT
}
share = {
sso_groups = ["group1", "group2", "group3"]
}
}

output "dbsnapper_target" {
value = dbsnapper_target.tf_example
}
184 changes: 184 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
module terraform-provider-dbsnapper

go 1.22

require (
github.com/google/uuid v1.6.0
github.com/hashicorp/terraform-plugin-docs v0.19.4
github.com/hashicorp/terraform-plugin-framework v1.9.0
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.8.0
github.com/joescharf/dbsnapper/v2 v2.7.2
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
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/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.27.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.7 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.9 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/charmbracelet/bubbles v0.18.0 // indirect
github.com/charmbracelet/bubbletea v0.26.2 // indirect
github.com/charmbracelet/lipgloss v0.10.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v26.1.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/cli v1.1.6 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-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.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hc-install v0.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.20.1 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx-zap v0.0.0-20221202020421-94b1cb2f889f // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/joescharf/go-datapipe v0.0.2 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microsoft/go-mssqldb v1.7.1 // 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/moby/docker-image-spec v1.3.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/panjf2000/ants v1.3.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/subosito/gotenv v1.6.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/xo/dburl v0.23.1 // indirect
github.com/yuin/goldmark v1.7.1 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// replace github.com/joescharf/dbsnapper/v2 v2.7.1 => /Users/joescharf/app/dbsnapper/agent
571 changes: 571 additions & 0 deletions go.sum

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions internal/provider/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package provider

import (
"context"
"fmt"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
dbsTargetModel "github.com/joescharf/dbsnapper/v2/models/target"
)

func TFToResourceModel(ctx context.Context, tf *TargetResourceModel) (*TargetResourceModel, error) {

// Initialize pointer fields
if tf.Snapshot == nil {
tf.Snapshot = new(targetSnapshotModel)
}
if tf.Share == nil {
tf.Share = new(targetShareModel)
}
if tf.Sanitize == nil {
tf.Sanitize = new(targetSanitizeModel)
}

// Read the list of SSOGroups from the Share attribute
if !tf.Share.SSOGroups.IsNull() {
elements := make([]types.String, 0, len(tf.Share.SSOGroups.Elements()))
diags := tf.Share.SSOGroups.ElementsAs(ctx, &elements, false)
if diags.HasError() {
return tf, fmt.Errorf("Error reading SSOGroups: %s", diags)
}
ctx = tflog.SetField(ctx, "ID", tf.ID.String())
ctx = tflog.SetField(ctx, "elements", elements)
tflog.Debug(ctx, "TFToResourceModel - TargetResourceModel")

}
return tf, nil
}

func ResourceModelToAPIRequest(ctx context.Context, resourceModel *TargetResourceModel) (*dbsTargetModel.Target, error) {
targetRequest := new(dbsTargetModel.Target)
targetRequest.Sanitize = dbsTargetModel.SanitizeCfg{}
targetRequest.Share = dbsTargetModel.ShareCfg{}

// Copy Snapshot fields
if resourceModel.Snapshot != nil {
targetRequest.Snapshot.SrcURL = resourceModel.Snapshot.SrcURL.ValueString()
targetRequest.Snapshot.DstURL = resourceModel.Snapshot.DstURL.ValueString()
targetRequest.Snapshot.SrcBytes = resourceModel.Snapshot.SrcBytes.ValueInt64()
}

// Copy the optional Sanitize fields
if resourceModel.Sanitize != nil {
targetRequest.Sanitize.DstURL = resourceModel.Sanitize.DstURL.ValueString()
targetRequest.Sanitize.Query = resourceModel.Sanitize.Query.ValueString()
}

// Copy the optional Share fields
if resourceModel.Share != nil {
// Read from the plan using ElementsAs
elements := make([]types.String, 0, len(resourceModel.Share.SSOGroups.Elements()))
diag := resourceModel.Share.SSOGroups.ElementsAs(ctx, &elements, false)
if diag.HasError() {
return targetRequest, fmt.Errorf("Error reading SSOGroups: %s", diag)
}
// Copy the elements into the API request
targetRequest.Share.SsoGroups = make([]string, len(elements))
for i, v := range elements {
targetRequest.Share.SsoGroups[i] = v.ValueString()
}
}

uid, _ := uuid.Parse(resourceModel.ID.ValueString())

targetRequest.ID = uid
targetRequest.Name = resourceModel.Name.ValueString()
targetRequest.Status = resourceModel.Status.ValueString()
targetRequest.Messages = resourceModel.Messages.ValueString()

ctx = tflog.SetField(ctx, "ID", targetRequest.ID.String())
tflog.Debug(ctx, "PlanToApiRequest - targetRequest")

return targetRequest, nil
}

func APIResponseToResourceModel(ctx context.Context, targetApiResponse *dbsTargetModel.Target, resourceModel *TargetResourceModel) (*TargetResourceModel, error) {
resourceModel.ID = types.StringValue(targetApiResponse.ID.String())
resourceModel.Name = types.StringValue(targetApiResponse.Name)
resourceModel.Status = types.StringValue(targetApiResponse.Status)
resourceModel.Messages = types.StringValue(targetApiResponse.Messages)

// Snapshot
resourceModel.Snapshot.SrcURL = types.StringValue(targetApiResponse.Snapshot.SrcURL)
resourceModel.Snapshot.DstURL = types.StringValue(targetApiResponse.Snapshot.DstURL)
resourceModel.Snapshot.SrcBytes = types.Int64Value(targetApiResponse.Snapshot.SrcBytes)

// Sanitize
if targetApiResponse.Sanitize != (dbsTargetModel.SanitizeCfg{}) {
resourceModel.Sanitize.DstURL = types.StringValue(targetApiResponse.Sanitize.DstURL)
resourceModel.Sanitize.Query = types.StringValue(targetApiResponse.Sanitize.Query)
}
// Share
if targetApiResponse.Share.SsoGroups != nil {
l, diag := types.ListValueFrom(ctx, types.StringType, targetApiResponse.Share.SsoGroups)
if diag.HasError() {
return resourceModel, fmt.Errorf("Error mapping SSOGroups: %s", diag)
}
resourceModel.Share.SSOGroups = l
}
resourceModel.CreatedAt = types.StringValue(targetApiResponse.CreatedAt)
resourceModel.UpdatedAt = types.StringValue(targetApiResponse.UpdatedAt)

return resourceModel, nil
}
22 changes: 22 additions & 0 deletions internal/provider/dbsnapper_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package provider

import (
"github.com/joescharf/dbsnapper/v2/apiv1"
)

type DBSnapper struct {
IsReady bool
API *apiv1.APIV1
}

func NewDBSnapper(authtoken, baseURL string) *DBSnapper {
api := apiv1.NewClient(authtoken, baseURL)

d := &DBSnapper{
API: api,
}

d.IsReady = api.IsReady()

return d
}
140 changes: 140 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"os"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// Ensure dbSnapperProvider satisfies various provider interfaces.
var _ provider.Provider = &dbSnapperProvider{}

const baseURLProduction = "https://app.dbsnapper.com/api/v3"

// dbSnapperProvider defines the provider implementation.
type dbSnapperProvider struct {
// version is set to the provider version on release, "dev" when the
// provider is built and ran locally, and "test" when running acceptance
// testing.
version string
}
type dbSnapperProviderModel struct {
AuthToken types.String `tfsdk:"authtoken"`
BaseURL types.String `tfsdk:"base_url"`
}

func (p *dbSnapperProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "dbsnapper"
resp.Version = p.version
}

func (p *dbSnapperProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"authtoken": schema.StringAttribute{
Description: "DBSnapper API Authtoken",
Optional: true,
Sensitive: true,
},
"base_url": schema.StringAttribute{
Description: "DBSnapper API Base URL - for internal testing only",
Optional: true,
},
},
}
}

func (p *dbSnapperProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config dbSnapperProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

if config.AuthToken.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("authtoken"),
"Unknown DBSnapper authtoken",
"The provider cannot create the DBSnapper API client as there is an unknown configuration value for the DBSnapper API Authtoken."+
" Either target apply the source of the value first, set the value statically in the configuration, or use the DBSNAPPER_AUTHTOKEN environment variable.",
)
}
if resp.Diagnostics.HasError() {
return
}

// Default to Env Vars / override with TF config value if set
authtoken := os.Getenv("DBSNAPPER_AUTHTOKEN")
baseURL := os.Getenv("DBSNAPPER_BASE_URL")

if !config.AuthToken.IsNull() {
authtoken = config.AuthToken.ValueString()
}
if !config.BaseURL.IsNull() {
baseURL = config.BaseURL.ValueString()
}

// If expected configurations are missing, return errors

// If the baseURL is not set, default to production
if baseURL == "" {
baseURL = baseURLProduction
}

if authtoken == "" {
resp.Diagnostics.AddAttributeError(path.Root("host"),
"Missing DBSnapper API AuthToken", "The provider cannot create the DBSnapper API client as there is a missing or empty value for the DBSnapper API authtoken. "+
"Set the host value in the configuration or use the DBSNAPPER_AUTHTOKEN environment variable. "+
"If either is already set, ensure the value is not empty.")
}
if resp.Diagnostics.HasError() {
return
}

// Create client with configuration values
dbs := NewDBSnapper(authtoken, baseURL)
if !dbs.IsReady {
resp.Diagnostics.AddError("Failed to create DBSnapper API client", "API Not Ready")
return
}

resp.DataSourceData = dbs
resp.ResourceData = dbs

}

func (p *dbSnapperProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewTargetResource,
}
}

func (p *dbSnapperProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewTargetsDataSource,
}
}

func (p *dbSnapperProvider) Functions(ctx context.Context) []func() function.Function {
return nil
}

func New(version string) func() provider.Provider {
return func() provider.Provider {
return &dbSnapperProvider{
version: version,
}
}
}
17 changes: 17 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
)

// testAccProtoV6ProviderFactories are used to instantiate a provider during
// acceptance testing. The factory function will be invoked for every Terraform
// CLI command executed to create a provider server to which the CLI can
// reattach.
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"dbsnapper": providerserver.NewProtocol6WithError(New("test")()),
}
334 changes: 334 additions & 0 deletions internal/provider/target_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// Ensure provider defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &targetResource{}
_ resource.ResourceWithConfigure = &targetResource{}
_ resource.ResourceWithImportState = &targetResource{}
)

func NewTargetResource() resource.Resource {
return &targetResource{}
}

// targetResource defines the resource implementation.
type targetResource struct {
client *DBSnapper
}

type TargetResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Status types.String `tfsdk:"status"`
Messages types.String `tfsdk:"messages"`
Snapshot *targetSnapshotModel `tfsdk:"snapshot"`
Sanitize *targetSanitizeModel `tfsdk:"sanitize"`
Share *targetShareModel `tfsdk:"share"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}

// targetSnapshotModel maps snapshot data.
type targetSnapshotModel struct {
SrcURL types.String `tfsdk:"src_url"`
DstURL types.String `tfsdk:"dst_url"`
SrcBytes types.Int64 `tfsdk:"src_bytes"`
}

// targetSanitizeModel maps sanitization data.
type targetSanitizeModel struct {
DstURL types.String `tfsdk:"dst_url"`
Query types.String `tfsdk:"query"`
}

// targetShareModel maps share data.
type targetShareModel struct {
SSOGroups types.List `tfsdk:"sso_groups"`
}

func (r *targetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_target"
}

func (r *targetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Target resource",

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the target",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"created_at": schema.StringAttribute{
Description: "The time the target was created",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The time the target was last updated",
Computed: true,
},
"name": schema.StringAttribute{
Description: "The name of the target",
Required: true,
},
"status": schema.StringAttribute{
Description: "The status of the target - determined by agent",
Computed: true,
},
"messages": schema.StringAttribute{
Description: "The error messages from the target - determined by agent",
Computed: true,
},

"snapshot": schema.SingleNestedAttribute{
Description: "The snapshot configuration",
Required: true,
Attributes: map[string]schema.Attribute{
"src_url": schema.StringAttribute{
Description: "The source URL for the target snapshot",
Required: true,
},
"dst_url": schema.StringAttribute{
Description: "The destination URL for the target snapshot (will be overwritten)",
Optional: true,
},
"src_bytes": schema.Int64Attribute{
Description: "The size of the source database in bytes",
Computed: true,
},
},
},

"sanitize": schema.SingleNestedAttribute{
Description: "The sanitize configuration",
Optional: true,
Attributes: map[string]schema.Attribute{
"dst_url": schema.StringAttribute{
Description: "The destination URL of the database used to sanitizea snapshot",
Optional: true,
},
"query": schema.StringAttribute{
Description: "The query used to sanitize the snapshot",
Optional: true,
},
},
},

"share": schema.SingleNestedAttribute{
Description: "The share configuration",
Optional: true,
Attributes: map[string]schema.Attribute{
"sso_groups": schema.ListAttribute{
Description: "The SSO groups that have access to the target snapshot",
Optional: true,
ElementType: types.StringType,
},
},
},
},
}
}

func (r *targetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*DBSnapper)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *DBSnapper, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

////////////////////////////// CREATE //////////////////////////////

// Create creates a new target resource.
func (r *targetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan = new(TargetResourceModel)

// 1. Read Terraform PLAN into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, plan)...)

if resp.Diagnostics.HasError() {
return
}

// Read Terraform PLAN data into the model
plan, err := TFToResourceModel(ctx, plan)

if err != nil {
resp.Diagnostics.AddError("Error reading Terraform plan", fmt.Sprintf("Unable to read Terraform plan, got error: %s", err))
return
}

// Generate API Request Body
targetApiRequest, err := ResourceModelToAPIRequest(ctx, plan)
if err != nil {
resp.Diagnostics.AddError("API Request Error", fmt.Sprintf("Unable to create API request body, got error: %s", err))
return
}

// Call API to create target
targetResponse, err := r.client.API.CreateTarget(targetApiRequest)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create target, got error: %s", err))
return
}

// API response to Terraform mapping
plan, err = APIResponseToResourceModel(ctx, targetResponse, plan)
if err != nil {
resp.Diagnostics.AddError("API Response Error", fmt.Sprintf("Unable to create target, got error: %s", err))
return
}

tflog.Info(ctx, "DBSnapper Provider: Create Target Resource")

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

////////////////////////////// READ //////////////////////////////

// Read refreshes the Terraform state with the latest data.
func (r *targetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state = new(TargetResourceModel)

// Read Terraform prior STATE data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)

if resp.Diagnostics.HasError() {
return
}

state, err := TFToResourceModel(ctx, state)
if err != nil {
resp.Diagnostics.AddError("Error reading Terraform state", fmt.Sprintf("Unable to read Terraform state, got error: %s", err))
return
}

// Call API to get refreshed target data
targetResponse, err := r.client.API.GetTarget(state.ID.ValueString())

if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read target, got error: %s\n Could not read Target ID: %s\n", err, state.ID.ValueString()))

return
}

// API response to Terraform mapping
state, err = APIResponseToResourceModel(ctx, targetResponse, state)
if err != nil {
resp.Diagnostics.AddError("API Response Error", fmt.Sprintf("Unable to create target, got error: %s", err))
return
}

tflog.Info(ctx, "DBSnapper Provider: Read Target Resource")

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

////////////////////////////// UPDATE //////////////////////////////

// Update updates the target resource.
func (r *targetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan = new(TargetResourceModel)

// Read Terraform PLAN data into the model.
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

if resp.Diagnostics.HasError() {
return
}

// Convert Terraform PLAN data into the model
plan, err := TFToResourceModel(ctx, plan)

if err != nil {
resp.Diagnostics.AddError("Error reading Terraform plan", fmt.Sprintf("Unable to read Terraform plan, got error: %s", err))
return
}

// Generate API Request Body
targetApiRequest, err := ResourceModelToAPIRequest(ctx, plan)
if err != nil {
resp.Diagnostics.AddError("API Request Error", fmt.Sprintf("Unable to create API request body, got error: %s", err))
return
}

// Update target via API
targetResponse, err := r.client.API.UpdateTarget(plan.ID.ValueString(), targetApiRequest)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Errorf("Unable to update target id: %s, got error: %w", targetResponse.ID.String(), err).Error())
return
}

// API response to Terraform mapping
plan, err = APIResponseToResourceModel(ctx, targetResponse, plan)
if err != nil {
resp.Diagnostics.AddError("API Response Error", fmt.Sprintf("Unable to update target, got error: %s", err))
return
}

tflog.Info(ctx, "DBSnapper Provider: Update Target Resource")

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

////////////////////////////// DELETE //////////////////////////////

// Delete deletes the target resource.
func (r *targetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data TargetResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// Delete target via API
err := r.client.API.DeleteTarget(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete target, got error: %s", err))
return
}
}

////////////////////////////// IMPORT STATE //////////////////////////////

// ImportState imports the target resource state.
func (r *targetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
84 changes: 84 additions & 0 deletions internal/provider/target_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package provider

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccTargetResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccTargetResourceConfig,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify number of items
resource.TestCheckResourceAttr("dbsnapper_target.test", "name", "tf_test"),
),
},
// ImportState testing
{
ResourceName: "dbsnapper_target.test",
ImportState: true,
ImportStateVerify: true,
// The last_updated attribute does not exist in the HashiCups
// API, therefore there is no value for it during import.
ImportStateVerifyIgnore: []string{"last_updated"},
},
// Update and Read testing
{
Config: testAccTargetResourceConfigUpdate,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify first order item updated
resource.TestCheckResourceAttr("dbsnapper_target.test", "name", "tf_test_update"),
),
},
// Delete testing automatically occurs in TestCase
},
})
}

const testAccTargetResourceConfig = `
resource "dbsnapper_target" "test" {
name = "tf_test"
snapshot = {
src_url = "postgres://user:pass@localhost:5432/tf_test"
dst_url = "postgres://user:pass@localhost:5432/tf_test_snap"
}
sanitize = {
dst_url = "postgres://user:pass@localhost:5432/tf_test_snap_sanitized"
query = <<EOT
DROP TABLE IF EXISTS dbsnapper_info;
CREATE TABLE dbsnapper_info (created_at timestamp, tags text []);
INSERT INTO dbsnapper_info (created_at, tags)
VALUES (NOW(), '{target:tf_test, src:terraform_test}');
EOT
}
share = {
sso_groups = ["group1", "group2", "group3"]
}
}
`
const testAccTargetResourceConfigUpdate = `
resource "dbsnapper_target" "test" {
name = "tf_test_update"
snapshot = {
src_url = "postgres://user:pass@localhost:5432/tf_test_update"
dst_url = "postgres://user:pass@localhost:5432/tf_test_update_snap"
}
sanitize = {
dst_url = "postgres://user:pass@localhost:5432/tf_test_snap_sanitized"
query = <<EOT
DROP TABLE IF EXISTS dbsnapper_info;
CREATE TABLE dbsnapper_info (created_at timestamp, tags text []);
INSERT INTO dbsnapper_info (created_at, tags)
VALUES (NOW(), '{target:tf_test, src:terraform_test}');
EOT
}
share = {
sso_groups = ["group4", "group5", "group6"]
}
}
`
197 changes: 197 additions & 0 deletions internal/provider/targets_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &TargetsDataSource{}
_ datasource.DataSourceWithConfigure = &TargetsDataSource{}
)

// NewTargetsDataSource is a helper function to simplify the provider implementation.
func NewTargetsDataSource() datasource.DataSource {
return &TargetsDataSource{}
}

// TargetsDataSource is the data source implementation.
type TargetsDataSource struct {
client *DBSnapper
}

// TargetsDataSourceModel maps the data source schema data.
type TargetsDataSourceModel struct {
Targets []TargetResourceModel `tfsdk:"targets"`
}

// Metadata returns the data source type name.
func (d *TargetsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_targets"
}

// Schema defines the schema for the data source.
// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/attributes/single-nested
func (d *TargetsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Targets data source",

Attributes: map[string]schema.Attribute{
"targets": schema.ListNestedAttribute{
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the target",
Computed: true,
},
"created_at": schema.StringAttribute{
Description: "The time the target was created",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The time the target was last updated",
Computed: true,
},
"name": schema.StringAttribute{
Description: "The name of the target",
Computed: true,
},
"status": schema.StringAttribute{
Description: "The status of the target - determined by agent",
Computed: true,
},
"messages": schema.StringAttribute{
Description: "The error messages from the target - determined by agent",
Computed: true,
},

"snapshot": schema.SingleNestedAttribute{
Description: "The snapshot configuration",
Computed: true,
Attributes: map[string]schema.Attribute{
"src_url": schema.StringAttribute{
Description: "The source URL for the target snapshot",
Computed: true,
},
"dst_url": schema.StringAttribute{
Description: "The destination URL for the target snapshot (will be overwritten)",
Computed: true,
},
"src_bytes": schema.Int64Attribute{
Description: "The size of the source database in bytes",
Computed: true,
},
},
},

"sanitize": schema.SingleNestedAttribute{
Description: "The sanitize configuration",
Computed: true,
Attributes: map[string]schema.Attribute{
"dst_url": schema.StringAttribute{
Description: "The destination URL of the database used to sanitizea snapshot",
Computed: true,
},
"query": schema.StringAttribute{
Description: "The query used to sanitize the snapshot",
Computed: true,
},
},
},
"share": schema.SingleNestedAttribute{
Description: "The share configuration",
Optional: true,
Attributes: map[string]schema.Attribute{
"sso_groups": schema.ListAttribute{
Description: "The SSO groups that have access to the target snapshot",
Optional: true,
ElementType: types.StringType,
},
},
},
},
},
},
},
}
}

// Read refreshes the Terraform state with the latest data.
func (d *TargetsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var state TargetsDataSourceModel

targets := d.client.API.GetTargetsTF()

// if err != nil {
// resp.Diagnostics.AddError("Error fetching targets", err.Error())
// return
// }
for _, target := range targets {
idstr := target.ID.String()
targetState := TargetResourceModel{

ID: types.StringValue(idstr),
Name: types.StringValue(target.Name),
Status: types.StringValue(target.Status),
Messages: types.StringValue(target.Messages),
Snapshot: &targetSnapshotModel{
SrcURL: types.StringValue(target.Snapshot.SrcURL),
DstURL: types.StringValue(target.Snapshot.DstURL),
// SrcBytes: types.Int64Value(target.Snapshot.SrcBytes),
},
Sanitize: &targetSanitizeModel{
DstURL: types.StringValue(target.Sanitize.DstURL),
Query: types.StringValue(target.Sanitize.Query),
},
}
// Share
if target.Share.SsoGroups != nil {
l, diag := types.ListValueFrom(ctx, types.StringType, target.Share.SsoGroups)
if diag.HasError() {
resp.Diagnostics.AddError("Error reading SSOGroups", "")
return

}
targetState.Share = new(targetShareModel)
targetState.Share.SSOGroups = l
}
targetState.CreatedAt = types.StringValue(target.CreatedAt)
targetState.UpdatedAt = types.StringValue(target.UpdatedAt)

state.Targets = append(state.Targets, targetState)
}

// Set state
diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Configure adds the provider configured client to the data source.
func (d *TargetsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Add a nil check when handling ProviderData because Terraform
// sets that data after it calls the ConfigureProvider RPC.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*DBSnapper)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *DBSnapper, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

d.client = client
}
34 changes: 34 additions & 0 deletions internal/provider/targets_data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccTargetsDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccTargetsDataSourceConfig,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.dbsnapper_targets.test", "targets.#", "2"),
resource.TestCheckResourceAttr("data.dbsnapper_targets.test", "targets.0.name", "tf_target_1"),
resource.TestCheckResourceAttr("data.dbsnapper_targets.test", "targets.0.share.sso_groups.#", "2"),
resource.TestCheckResourceAttr("data.dbsnapper_targets.test", "targets.0.share.sso_groups.0", "target1"),
resource.TestCheckResourceAttr("data.dbsnapper_targets.test", "targets.1.name", "tf_target_2"),
),
},
},
})
}

const testAccTargetsDataSourceConfig = `
data "dbsnapper_targets" "test" {
}
`
53 changes: 53 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package main

import (
"context"
"flag"
"log"
"terraform-provider-dbsnapper/internal/provider"

"github.com/hashicorp/terraform-plugin-framework/providerserver"
)

// Run "go generate" to format example terraform files and generate the docs for the registry/website

// If you do not have terraform installed, you can remove the formatting command, but its suggested to
// ensure the documentation is formatted properly.
//go:generate terraform fmt -recursive ./examples/

// Run the docs generation tool, check its repository for more information on how it works and how docs
// can be customized.
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate -provider-name dbsnapper

var (
// these will be set by the goreleaser configuration
// to appropriate values for the compiled binary.
version string = "dev"

// goreleaser can pass other information to the main package, such as the specific commit
// https://goreleaser.com/cookbooks/using-main.version/
)

func main() {
var debug bool

flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
flag.Parse()

opts := providerserver.ServeOpts{
// TODO: Update this string with the published name of your provider.
// Also update the tfplugindocs generate command to either remove the
// -provider-name flag or set its value to the updated provider name.
Address: "registry.terraform.io/hashicorp/dbsnapper",
Debug: debug,
}

err := providerserver.Serve(context.Background(), provider.New(version), opts)

if err != nil {
log.Fatal(err.Error())
}
}
6 changes: 6 additions & 0 deletions terraform-registry-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"metadata": {
"protocol_versions": ["6.0"]
}
}
11 changes: 11 additions & 0 deletions tools/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

//go:build tools

package tools

import (
// Documentation generation
_ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs"
)

0 comments on commit 8c65bd7

Please sign in to comment.