Overhaul VirtFusion provider: 20 resources, 30 data sources, multipage pagination
Complete rewrite of the VirtFusion Terraform provider with full API coverage: - 20 managed resources (server, build, SSH key, user, firewall, IP blocks, etc.) - 30 data sources (hypervisors, packages, servers, IP blocks, self-service, etc.) - New HTTP client with proper error handling, query parameter support, and automatic multipage pagination via GetAllPages (fetches all pages from Laravel-style paginated endpoints and merges into a single response) - Fixed type mismatches against live API: ServerData.Suspended (int→bool), IPBlockData.Type (string→int), PackageData json tags (primaryStorage, etc.), ServerData nested CPU/Settings/Resources structure, HypervisorGroupResources array response - Configurable results-per-page (default 300) on all list data sources - Migrated CI from GitHub Actions to Gitea Actions - Updated goreleaser config, go.mod dependencies, and examples Verified against live VirtFusion instance at cp.vps.ezscale.tech: all data sources return correct data with full pagination support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +0,0 @@
|
|||||||
# 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",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
33
.gitea/workflows/ci.yaml
Normal file
33
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -race -v ./internal/...
|
||||||
|
|
||||||
|
- name: Check go generate
|
||||||
|
run: |
|
||||||
|
go generate ./...
|
||||||
|
git diff --exit-code || (echo "go generate produced changes; please run 'go generate ./...' and commit" && exit 1)
|
||||||
22
.gitea/workflows/endpoint-sync.yaml
Normal file
22
.gitea/workflows/endpoint-sync.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Endpoint Sync Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * 1'
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'openapi.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-drift:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Check endpoint drift
|
||||||
|
run: go run ./scripts/check-endpoint-drift.go
|
||||||
49
.gitea/workflows/release.yaml
Normal file
49
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
id: import_gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v6
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
passphrase: ${{ secrets.PASSPHRASE }}
|
||||||
|
|
||||||
|
- name: Check endpoint drift
|
||||||
|
run: go run ./scripts/check-endpoint-drift.go
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF#refs/tags/}"
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${TAG}\", \"body\": \"Release ${TAG} — see GitHub mirror for artifacts.\"}" \
|
||||||
|
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
|
||||||
25
.gitea/workflows/version-check.yaml
Normal file
25
.gitea/workflows/version-check.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Version Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'main.go'
|
||||||
|
- 'internal/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check version bump
|
||||||
|
run: |
|
||||||
|
BASE_VERSION=$(git show origin/${{ github.base_ref }}:main.go | grep -oP 'version string = "\K[^"]+')
|
||||||
|
HEAD_VERSION=$(grep -oP 'version string = "\K[^"]+' main.go)
|
||||||
|
if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
|
||||||
|
echo "::warning::Version in main.go has not been bumped (still ${HEAD_VERSION}). Consider updating it for this release."
|
||||||
|
else
|
||||||
|
echo "Version bumped: ${BASE_VERSION} → ${HEAD_VERSION}"
|
||||||
|
fi
|
||||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
|||||||
* @hashicorp/terraform-devex
|
|
||||||
5
.github/CODE_OF_CONDUCT.md
vendored
5
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,5 +0,0 @@
|
|||||||
# 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
|
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -1,8 +0,0 @@
|
|||||||
# 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"
|
|
||||||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
# Terraform Provider release workflow.
|
|
||||||
name: Release
|
|
||||||
|
|
||||||
# This GitHub action creates a release when a tag that matches the pattern
|
|
||||||
# "v*" (e.g. v0.1.0) is created.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
# Releases need permissions to read and write the repository contents.
|
|
||||||
# GitHub considers creating releases and uploading assets as writing contents.
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
goreleaser:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
with:
|
|
||||||
# Allow goreleaser to access older tag information.
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
- name: Import GPG key
|
|
||||||
uses: crazy-max/ghaction-import-gpg@72b6676b71ab476b77e676928516f6982eef7a41 # v5.3.0
|
|
||||||
id: import_gpg
|
|
||||||
with:
|
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
||||||
passphrase: ${{ secrets.PASSPHRASE }}
|
|
||||||
- name: Run GoReleaser
|
|
||||||
uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.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 }}
|
|
||||||
81
.github/workflows/test.yml
vendored
81
.github/workflows/test.yml
vendored
@@ -1,81 +0,0 @@
|
|||||||
# Terraform Provider testing workflow.
|
|
||||||
name: Tests
|
|
||||||
|
|
||||||
# This GitHub action runs your tests for each pull request and push.
|
|
||||||
# Optionally, you can turn it on using a schedule for regular testing.
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- 'README.md'
|
|
||||||
push:
|
|
||||||
paths-ignore:
|
|
||||||
- 'README.md'
|
|
||||||
|
|
||||||
# Testing only needs permissions to read the repository contents.
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Ensure project builds before running testing matrix
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
- run: go mod download
|
|
||||||
- run: go build -v .
|
|
||||||
- name: Run linters
|
|
||||||
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299 # v3.6.0
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
generate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
- run: go generate ./...
|
|
||||||
- name: git diff
|
|
||||||
run: |
|
|
||||||
git diff --compact-summary --exit-code || \
|
|
||||||
(echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1)
|
|
||||||
|
|
||||||
# Run acceptance tests in a matrix with Terraform CLI versions
|
|
||||||
test:
|
|
||||||
name: Terraform Provider Acceptance Tests
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# list whatever Terraform versions here you would like to support
|
|
||||||
terraform:
|
|
||||||
- '1.0.*'
|
|
||||||
- '1.1.*'
|
|
||||||
- '1.2.*'
|
|
||||||
- '1.3.*'
|
|
||||||
- '1.4.*'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
- uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
|
|
||||||
with:
|
|
||||||
terraform_version: ${{ matrix.terraform }}
|
|
||||||
terraform_wrapper: false
|
|
||||||
- run: go mod download
|
|
||||||
- env:
|
|
||||||
TF_ACC: "1"
|
|
||||||
run: go test -v -cover ./internal/provider/
|
|
||||||
timeout-minutes: 10
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,5 +31,8 @@ website/vendor
|
|||||||
!command/test-fixtures/**/*.tfstate
|
!command/test-fixtures/**/*.tfstate
|
||||||
!command/test-fixtures/**/.terraform/
|
!command/test-fixtures/**/.terraform/
|
||||||
|
|
||||||
|
# Local test directory
|
||||||
|
test/
|
||||||
|
|
||||||
# Keep windows files with windows line endings
|
# Keep windows files with windows line endings
|
||||||
*.winfile eol=crlf
|
*.winfile eol=crlf
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Visit https://golangci-lint.run/ for usage documentation
|
|
||||||
# and information on other useful linters
|
|
||||||
issues:
|
issues:
|
||||||
max-per-linter: 0
|
max-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
@@ -9,13 +7,11 @@ linters:
|
|||||||
enable:
|
enable:
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- errcheck
|
- errcheck
|
||||||
- exportloopref
|
|
||||||
- forcetypeassert
|
- forcetypeassert
|
||||||
- godot
|
- godot
|
||||||
- gofmt
|
- gofmt
|
||||||
- gosimple
|
- gosimple
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- makezero
|
|
||||||
- misspell
|
- misspell
|
||||||
- nilerr
|
- nilerr
|
||||||
- predeclared
|
- predeclared
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
# Visit https://goreleaser.com for documentation on how to customize this
|
version: 2
|
||||||
# behavior.
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
# this is just an example and not a requirement for provider building/publishing
|
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- env:
|
||||||
# goreleaser does not work with CGO, it could also complicate
|
|
||||||
# usage by users in CI/CD systems like Terraform Cloud where
|
|
||||||
# they are unable to install libraries.
|
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
flags:
|
flags:
|
||||||
@@ -29,32 +26,41 @@ builds:
|
|||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: '386'
|
goarch: '386'
|
||||||
binary: '{{ .ProjectName }}_v{{ .Version }}'
|
binary: '{{ .ProjectName }}_v{{ .Version }}'
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format: zip
|
- format: zip
|
||||||
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
|
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
extra_files:
|
extra_files:
|
||||||
- glob: 'terraform-registry-manifest.json'
|
- glob: 'terraform-registry-manifest.json'
|
||||||
name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
|
name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
|
||||||
name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
|
name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
|
||||||
algorithm: sha256
|
algorithm: sha256
|
||||||
|
|
||||||
signs:
|
signs:
|
||||||
- artifacts: checksum
|
- artifacts: checksum
|
||||||
args:
|
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"
|
- "--batch"
|
||||||
- "--local-user"
|
- "--local-user"
|
||||||
- "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
|
- "{{ .Env.GPG_FINGERPRINT }}"
|
||||||
- "--output"
|
- "--output"
|
||||||
- "${signature}"
|
- "${signature}"
|
||||||
- "--detach-sign"
|
- "--detach-sign"
|
||||||
- "${artifact}"
|
- "${artifact}"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
github:
|
||||||
|
owner: EZSCALE
|
||||||
|
name: terraform-provider-virtfusion
|
||||||
extra_files:
|
extra_files:
|
||||||
- glob: 'terraform-registry-manifest.json'
|
- glob: 'terraform-registry-manifest.json'
|
||||||
name_template: '{{ .ProjectName }}_{{ .Version }}_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:
|
changelog:
|
||||||
skip: true
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
- '^ci:'
|
||||||
|
|||||||
23
GNUmakefile
23
GNUmakefile
@@ -1,6 +1,25 @@
|
|||||||
default: testacc
|
default: build
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
go build -v ./...
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -race -v ./internal/...
|
||||||
|
|
||||||
# Run acceptance tests
|
|
||||||
.PHONY: testacc
|
.PHONY: testacc
|
||||||
testacc:
|
testacc:
|
||||||
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
|
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
.PHONY: generate
|
||||||
|
generate:
|
||||||
|
go generate ./...
|
||||||
|
|
||||||
|
.PHONY: drift-check
|
||||||
|
drift-check:
|
||||||
|
go run ./scripts/check-endpoint-drift.go
|
||||||
|
|||||||
506
endpoint-manifest.json
Normal file
506
endpoint-manifest.json
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/backups/server/{serverId}",
|
||||||
|
"summary": "Retrieve a server backups",
|
||||||
|
"tag": "Backups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/compute/hypervisors",
|
||||||
|
"summary": "Retrieve hypervisors",
|
||||||
|
"tag": "Hypervisors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/compute/hypervisors/{hypervisorId}",
|
||||||
|
"summary": "Retrive a Hypervisor",
|
||||||
|
"tag": "Hypervisors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/compute/hypervisors/groups",
|
||||||
|
"summary": "Retrieve hypervisor groups",
|
||||||
|
"tag": "Hypervisor Groups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/compute/hypervisors/groups/{hypervisorGroupId}",
|
||||||
|
"summary": "Retrieve a hypervisor group",
|
||||||
|
"tag": "Hypervisor Groups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/compute/hypervisors/groups/{hypervisorGroupId}/resources",
|
||||||
|
"summary": "Retrieve a hypervisor groups resources",
|
||||||
|
"tag": "Hypervisor Groups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/connect",
|
||||||
|
"summary": "Test connection",
|
||||||
|
"tag": "General"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/connectivity/ipblocks",
|
||||||
|
"summary": "Retrieve IP blocks",
|
||||||
|
"tag": "IP Blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/connectivity/ipblocks/{blockId}",
|
||||||
|
"summary": "Retrieve an IP block",
|
||||||
|
"tag": "IP Blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/connectivity/ipblocks/{blockId}/ipv4",
|
||||||
|
"summary": "Add an IPv4 range to an IP block",
|
||||||
|
"tag": "IP Blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/dns/services/{serviceId}",
|
||||||
|
"summary": "Retrieve a DNS service",
|
||||||
|
"tag": "DNS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/media/iso/{isoId}",
|
||||||
|
"summary": "Retrieve an ISO",
|
||||||
|
"tag": "Media"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/media/templates/fromServerPackageSpec/{serverPackageId}",
|
||||||
|
"summary": "Retrieve operating system templates that are available for a package",
|
||||||
|
"tag": "Media"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/packages",
|
||||||
|
"summary": "Retrieve packages",
|
||||||
|
"tag": "Packages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/packages/{packageId}",
|
||||||
|
"summary": "Retrieve a packge",
|
||||||
|
"tag": "Packages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/queue/{queueId}",
|
||||||
|
"summary": "Retrieve a queue item",
|
||||||
|
"tag": "Queue & Tasks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/selfService/access/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Modify user access",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/selfService/credit/{creditId}",
|
||||||
|
"summary": "Cancel credit that was applied to a user",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/selfService/credit/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Add credit to user",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/selfService/currencies",
|
||||||
|
"summary": "Retrieve currencies",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/selfService/hourlyGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Remove hourly group profile from a user",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/selfService/hourlyGroupProfile/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Add an hourly group profile to a user",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/selfService/hourlyResourcePack/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Set an hourly resource pack",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/selfService/hourlyStats/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Retrieve hourly statistics",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/selfService/report/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Generate a report",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/selfService/resourceGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Remove resource group from a user",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/selfService/resourceGroupProfile/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Add a resource group profile to a user",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/selfService/resourcePack/{packId}",
|
||||||
|
"summary": "Delete a user resource pack",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/selfService/resourcePack/{packId}",
|
||||||
|
"summary": "Retrieve a user resource pack",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/selfService/resourcePack/{packId}",
|
||||||
|
"summary": "Modify user resource pack",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/selfService/resourcePack/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Add a resource pack to a user",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/selfService/resourcePackServers/{packId}",
|
||||||
|
"summary": "Delete all servers attached to a pack ID",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/selfService/resourcePackServers/{packId}/suspend",
|
||||||
|
"summary": "Suspend all servers assigned to a reosurce pack",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/selfService/resourcePackServers/{packId}/unsuspend",
|
||||||
|
"summary": "Unsuspend all servers assigned to a reosurce pack",
|
||||||
|
"tag": "Self Service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/selfService/usage/byUserExtRelationId/{extRelationId}",
|
||||||
|
"summary": "Retrieve a users usage",
|
||||||
|
"tag": "Self Service/External Relational ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers",
|
||||||
|
"summary": "Retrieve servers",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers",
|
||||||
|
"summary": "Create a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/servers/{serverId}",
|
||||||
|
"summary": "Delete a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/{serverId}",
|
||||||
|
"summary": "Retrieve a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/backups/plan/{planId}",
|
||||||
|
"summary": "Add, remove or modify a backup plan",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/build",
|
||||||
|
"summary": "Build a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/customXML",
|
||||||
|
"summary": "Set custom XML",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/{serverId}/firewall/{interface}",
|
||||||
|
"summary": "Retrieve firewall",
|
||||||
|
"tag": "Servers/Network/Firewall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/firewall/{interface}/disable",
|
||||||
|
"summary": "Disable firewall",
|
||||||
|
"tag": "Servers/Network/Firewall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/firewall/{interface}/enable",
|
||||||
|
"summary": "Enable firewall",
|
||||||
|
"tag": "Servers/Network/Firewall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/firewall/{interface}/rules",
|
||||||
|
"summary": "Apply firewall rulesets",
|
||||||
|
"tag": "Servers/Network/Firewall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/servers/{serverId}/ipv4",
|
||||||
|
"summary": "Remove an array of IPv4 addresses",
|
||||||
|
"tag": "Servers/Network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/ipv4",
|
||||||
|
"summary": "Add an array of IPv4 addresses",
|
||||||
|
"tag": "Servers/Network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/ipv4Qty",
|
||||||
|
"summary": "Add a quantity of IPv4 addresses",
|
||||||
|
"tag": "Servers/Network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/modify/cpuCores",
|
||||||
|
"summary": "Modify CPU cores",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/modify/cpuThrottle",
|
||||||
|
"summary": "Throttle a servers CPU",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/modify/memory",
|
||||||
|
"summary": "Modify memory",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/modify/name",
|
||||||
|
"summary": "Modify name",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/modify/traffic",
|
||||||
|
"summary": "Modify primary traffic allowance",
|
||||||
|
"tag": "Servers/Network/Traffic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/servers/{serverId}/networkWhitelist",
|
||||||
|
"summary": "Remove an address from the whitelist",
|
||||||
|
"tag": "Servers/Network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/networkWhitelist",
|
||||||
|
"summary": "Add an address to the whitelist",
|
||||||
|
"tag": "Servers/Network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/owner/{newOwnerId}",
|
||||||
|
"summary": "Change owner",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/servers/{serverId}/package/{packageId}",
|
||||||
|
"summary": "Change a server package",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/power/boot",
|
||||||
|
"summary": "Boot a server",
|
||||||
|
"tag": "Servers/Power"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/power/poweroff",
|
||||||
|
"summary": "Poweroff a server",
|
||||||
|
"tag": "Servers/Power"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/power/restart",
|
||||||
|
"summary": "Restart a server",
|
||||||
|
"tag": "Servers/Power"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/power/shutdown",
|
||||||
|
"summary": "Shutdown a server",
|
||||||
|
"tag": "Servers/Power"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/resetPassword",
|
||||||
|
"summary": "Reset a server password",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/suspend",
|
||||||
|
"summary": "Suspend a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/{serverId}/templates",
|
||||||
|
"summary": "Retrieve OS templates available to a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/{serverId}/traffic",
|
||||||
|
"summary": "Retrieve a servers traffic statistics",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/{serverId}/traffic/blocks",
|
||||||
|
"summary": "Retrieve a servers traffic blocks",
|
||||||
|
"tag": "Servers/Network/Traffic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/traffic/blocks",
|
||||||
|
"summary": "Add a traffic block to a server",
|
||||||
|
"tag": "Servers/Network/Traffic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/servers/{serverId}/traffic/blocks/{blockId}",
|
||||||
|
"summary": "Remove a traffic block from a server",
|
||||||
|
"tag": "Servers/Network/Traffic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/unsuspend",
|
||||||
|
"summary": "Unsuspend a server",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/{serverId}/vnc",
|
||||||
|
"summary": "Retrive VNC details",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/servers/{serverId}/vnc",
|
||||||
|
"summary": "Enable or disable VNC",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/servers/user/{userId}",
|
||||||
|
"summary": "Retrieve a users servers",
|
||||||
|
"tag": "Servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/ssh_keys",
|
||||||
|
"summary": "Add an SSH key to a user account",
|
||||||
|
"tag": "SSH Keys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/ssh_keys/{keyId}",
|
||||||
|
"summary": "Delete an SSH key from a user",
|
||||||
|
"tag": "SSH Keys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/ssh_keys/{keyId}",
|
||||||
|
"summary": "Retrieve an SSH key",
|
||||||
|
"tag": "SSH Keys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/ssh_keys/user/{userId}",
|
||||||
|
"summary": "Retrieve a users SSH keys",
|
||||||
|
"tag": "SSH Keys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/users",
|
||||||
|
"summary": "Create a user",
|
||||||
|
"tag": "Users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/users/{extRelationId}/authenticationTokens",
|
||||||
|
"summary": "Generate a set of login tokens",
|
||||||
|
"tag": "Users/External Rel ID & Rel Str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/users/{extRelationId}/byExtRelation",
|
||||||
|
"summary": "Delete a user",
|
||||||
|
"tag": "Users/External Rel ID & Rel Str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/users/{extRelationId}/byExtRelation",
|
||||||
|
"summary": "Retrieve a user",
|
||||||
|
"tag": "Users/External Rel ID & Rel Str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/users/{extRelationId}/byExtRelation",
|
||||||
|
"summary": "Modify a user",
|
||||||
|
"tag": "Users/External Rel ID & Rel Str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/users/{extRelationId}/byExtRelation/resetPassword",
|
||||||
|
"summary": "Change a user passowrd",
|
||||||
|
"tag": "Users/External Rel ID & Rel Str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/users/{extRelationId}/serverAuthenticationTokens/{serverId}",
|
||||||
|
"summary": "Generate a set of loging tokens using a server ID",
|
||||||
|
"tag": "Users/External Rel ID & Rel Str"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
data "scaffolding_example" "example" {
|
|
||||||
configurable_attribute = "some-value"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
data "virtfusion_hypervisors" "all" {}
|
||||||
|
|
||||||
|
output "hypervisor_names" {
|
||||||
|
value = [for h in data.virtfusion_hypervisors.all.hypervisors : h.name]
|
||||||
|
}
|
||||||
5
examples/data-sources/virtfusion_packages/data-source.tf
Normal file
5
examples/data-sources/virtfusion_packages/data-source.tf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
data "virtfusion_packages" "all" {}
|
||||||
|
|
||||||
|
output "package_names" {
|
||||||
|
value = [for p in data.virtfusion_packages.all.packages : p.name]
|
||||||
|
}
|
||||||
7
examples/data-sources/virtfusion_server/data-source.tf
Normal file
7
examples/data-sources/virtfusion_server/data-source.tf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
data "virtfusion_server" "example" {
|
||||||
|
id = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
output "server_name" {
|
||||||
|
value = data.virtfusion_server.example.name
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
provider "virtfusion" {
|
provider "virtfusion" {
|
||||||
endpoint = "example.com"
|
endpoint = "https://cp.example.com"
|
||||||
api_token = "myapikey"
|
api_token = var.virtfusion_api_token
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "virtfusion_api_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
resource "virtfusion_build" "node1" {
|
|
||||||
server_id = virtfusion_server.node1.id
|
|
||||||
name = "node1-demo"
|
|
||||||
hostname = "node1.example.com"
|
|
||||||
osid = 1
|
|
||||||
vnc = true
|
|
||||||
ipv6 = true
|
|
||||||
ssh_keys = [virtfusion_ssh.dummy_key.id]
|
|
||||||
email = true
|
|
||||||
}
|
|
||||||
@@ -12,3 +12,7 @@ resource "virtfusion_server" "node1" {
|
|||||||
storage_profile = 1
|
storage_profile = 1
|
||||||
network_profile = 1
|
network_profile = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "server_id" {
|
||||||
|
value = virtfusion_server.node1.id
|
||||||
|
}
|
||||||
|
|||||||
10
examples/resources/virtfusion_server_build/resource.tf
Normal file
10
examples/resources/virtfusion_server_build/resource.tf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
resource "virtfusion_server_build" "node1_build" {
|
||||||
|
server_id = virtfusion_server.node1.id
|
||||||
|
name = "my-server"
|
||||||
|
hostname = "my-server.example.com"
|
||||||
|
osid = 1
|
||||||
|
vnc = true
|
||||||
|
ipv6 = false
|
||||||
|
ssh_keys = [virtfusion_ssh_key.mykey.id]
|
||||||
|
email = false
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
resource "virtfusion_ssh" "dummy_key" {
|
|
||||||
# This is what is displayed in the UI on the SSH keys page.
|
|
||||||
name = "dummy_key"
|
|
||||||
|
|
||||||
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCRM5gzj6BpVbTEZ8XX5meQOC9X+znTMCQbXTfdqm9IP3HY2JbqH+yfCBWSsLpXim6WvsYtfkAhrtrkdmaX66Wn1uo6XvARwi/5D1VRTM94vwoitJb0rne4OorpwGIGCpDIi1iRA/ERIbAIQpw/2PJfm7q+fEj9TS+n/MzYOOmwTaKPEJ8+wHwXbjcSNoBQmEPonafbQKQN5PXe5rwnTNAqJWhGPHqF2t7lvZy+m7Sl7X1vUVlw+7iZzOVm9iDXmUInc8A0kz18l/O+4ELhRxxzjmSX5/KkN0GG7wS7CHlq9MS2741MS6p0ZNMgTT/04RfsY5JXoOa1gCeAdnXQST9ylvBd6hXubV95lRM8AXAhEJFHpa0Xn1gHMJ4F0cjjvmBIDx39QztuYsNJPk8veBBQwhOzhnJ3Zh2IYTQD+Mwu5yUrJzUt7ia8X5fhjbrYlfUgdH+siBbvJRzyXwnZdHArher55U4xPCJO4qRrFr72Jn+WGzkcY53oLnW5K3NnPaYViCJD2BgJZU1YF8oA3RyEG+2GS7Ksqs2nXXlZ1c+RXLUXM0pxDrwqvYrE3Ae+O/PtZ0cqpesyjxDfH/R2cj86jjdEi7S8nhgkumHwkoac8LCJnoAeC9S7sxmI99VBHcNwCazx3ZL2UAI3Ik/DQBZXcCPXw9MfY25SyQwEYftMKw== dummy_key"
|
|
||||||
|
|
||||||
# This is the user ID that the key will be associated with.
|
|
||||||
user_id = 1
|
|
||||||
}
|
|
||||||
9
examples/resources/virtfusion_ssh_key/resource.tf
Normal file
9
examples/resources/virtfusion_ssh_key/resource.tf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
resource "virtfusion_ssh_key" "mykey" {
|
||||||
|
user_id = 1
|
||||||
|
name = "my-ssh-key"
|
||||||
|
public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample user@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ssh_key_id" {
|
||||||
|
value = virtfusion_ssh_key.mykey.id
|
||||||
|
}
|
||||||
5
examples/resources/virtfusion_user/resource.tf
Normal file
5
examples/resources/virtfusion_user/resource.tf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
resource "virtfusion_user" "customer1" {
|
||||||
|
name = "John Doe"
|
||||||
|
email = "john@example.com"
|
||||||
|
ext_relation_id = "cust-12345"
|
||||||
|
}
|
||||||
76
go.mod
76
go.mod
@@ -1,63 +1,71 @@
|
|||||||
module terraform-provider-virtfusion
|
module terraform-provider-virtfusion
|
||||||
|
|
||||||
go 1.19
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/hashicorp/terraform-plugin-docs v0.16.0
|
github.com/hashicorp/terraform-plugin-docs v0.19.4
|
||||||
github.com/hashicorp/terraform-plugin-framework v1.3.5
|
github.com/hashicorp/terraform-plugin-framework v1.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
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/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
github.com/armon/go-radix v1.0.0 // indirect
|
github.com/armon/go-radix v1.0.0 // indirect
|
||||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/cloudflare/circl v1.3.7 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/cli v1.1.6 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
|
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-plugin v1.4.10 // indirect
|
github.com/hashicorp/go-plugin v1.6.2 // indirect
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/hashicorp/hc-install v0.5.2 // indirect
|
github.com/hashicorp/hc-install v0.7.0 // indirect
|
||||||
github.com/hashicorp/terraform-exec v0.18.1 // indirect
|
github.com/hashicorp/terraform-exec v0.21.0 // indirect
|
||||||
github.com/hashicorp/terraform-json v0.17.1 // indirect
|
github.com/hashicorp/terraform-json v0.22.1 // indirect
|
||||||
github.com/hashicorp/terraform-plugin-go v0.18.0 // indirect
|
github.com/hashicorp/terraform-plugin-go v0.25.0 // indirect
|
||||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||||
github.com/hashicorp/terraform-registry-address v0.2.1 // indirect
|
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
|
||||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.3.3 // indirect
|
||||||
github.com/imdario/mergo v0.3.13 // indirect
|
github.com/imdario/mergo v0.3.15 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/oklog/run v1.0.0 // indirect
|
github.com/oklog/run v1.0.0 // indirect
|
||||||
github.com/posener/complete v1.2.3 // indirect
|
github.com/posener/complete v1.2.3 // indirect
|
||||||
github.com/russross/blackfriday v1.6.0 // indirect
|
|
||||||
github.com/shopspring/decimal v1.3.1 // indirect
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/zclconf/go-cty v1.13.2 // indirect
|
github.com/yuin/goldmark v1.7.1 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // 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
|
||||||
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||||
golang.org/x/mod v0.11.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||||
google.golang.org/grpc v1.56.1 // indirect
|
google.golang.org/grpc v1.67.1 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
238
go.sum
238
go.sum
@@ -1,45 +1,60 @@
|
|||||||
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
|
||||||
|
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
|
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
|
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
|
||||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8=
|
||||||
|
github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -53,56 +68,59 @@ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
|
|||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk=
|
github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
|
||||||
github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0=
|
github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
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 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0=
|
github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk=
|
||||||
github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI=
|
github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA=
|
||||||
github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4=
|
github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
|
||||||
github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980=
|
github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
|
||||||
github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA=
|
github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
|
||||||
github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o=
|
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
|
||||||
github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI=
|
github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c=
|
||||||
github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA=
|
github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA=
|
||||||
github.com/hashicorp/terraform-plugin-framework v1.3.5 h1:FJ6s3CVWVAxlhiF/jhy6hzs4AnPHiflsp9KgzTGl1wo=
|
github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw=
|
||||||
github.com/hashicorp/terraform-plugin-framework v1.3.5/go.mod h1:2gGDpWiTI0irr9NSTLFAKlTi6KwGti3AoU19rFqU30o=
|
github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU=
|
||||||
github.com/hashicorp/terraform-plugin-go v0.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE=
|
github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks=
|
||||||
github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y=
|
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 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
|
||||||
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
||||||
github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM=
|
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
|
||||||
github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y=
|
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 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
|
||||||
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
||||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
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 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
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-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
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/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
@@ -114,19 +132,20 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
|
|||||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
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 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
|
||||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=
|
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/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||||
@@ -134,63 +153,86 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
|
||||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
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 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||||
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||||
|
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
|
||||||
|
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
||||||
|
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||||
|
go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
|
||||||
|
go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
|
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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-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-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-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-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||||
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
71
internal/client/client.go
Normal file
71
internal/client/client.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the VirtFusion API client.
|
||||||
|
type Client struct {
|
||||||
|
BaseURL string
|
||||||
|
Token string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new VirtFusion API client.
|
||||||
|
// The endpoint can be a hostname (e.g. "cp.example.com") or a full URL
|
||||||
|
// (e.g. "https://cp.example.com" or "https://cp.example.com/api/v1").
|
||||||
|
func New(endpoint, token string) (*Client, error) {
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil, fmt.Errorf("endpoint is required")
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("api_token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := normalizeEndpoint(endpoint)
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Token: token,
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeEndpoint takes a user-provided endpoint and returns a full base URL
|
||||||
|
// ending with /api/v1. Supports:
|
||||||
|
// - "cp.example.com" → "https://cp.example.com/api/v1"
|
||||||
|
// - "https://cp.example.com" → "https://cp.example.com/api/v1"
|
||||||
|
// - "https://cp.example.com/api/v1" → "https://cp.example.com/api/v1"
|
||||||
|
func normalizeEndpoint(endpoint string) string {
|
||||||
|
endpoint = strings.TrimRight(endpoint, "/")
|
||||||
|
|
||||||
|
// If no scheme, add https://
|
||||||
|
if !strings.Contains(endpoint, "://") {
|
||||||
|
endpoint = "https://" + endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse to validate
|
||||||
|
u, err := url.Parse(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to simple construction
|
||||||
|
return endpoint + "/api/v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If path already ends with /api/v1, use as-is
|
||||||
|
if strings.HasSuffix(u.Path, "/api/v1") {
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise append /api/v1
|
||||||
|
u.Path = strings.TrimRight(u.Path, "/") + "/api/v1"
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
34
internal/client/errors.go
Normal file
34
internal/client/errors.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// APIError represents an error returned by the VirtFusion API.
|
||||||
|
type APIError struct {
|
||||||
|
StatusCode int
|
||||||
|
Status string
|
||||||
|
Body string
|
||||||
|
Errors map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
if len(e.Errors) > 0 {
|
||||||
|
return fmt.Sprintf("VirtFusion API error %d (%s): %v", e.StatusCode, e.Status, e.Errors)
|
||||||
|
}
|
||||||
|
if e.Body != "" {
|
||||||
|
return fmt.Sprintf("VirtFusion API error %d (%s): %s", e.StatusCode, e.Status, e.Body)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("VirtFusion API error %d (%s)", e.StatusCode, e.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNotFound returns true if the error is a 404 Not Found response.
|
||||||
|
func (e *APIError) IsNotFound() bool {
|
||||||
|
return e.StatusCode == 404
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidationError returns true if the error is a 422 Unprocessable Entity response.
|
||||||
|
func (e *APIError) IsValidationError() bool {
|
||||||
|
return e.StatusCode == 422
|
||||||
|
}
|
||||||
192
internal/client/request.go
Normal file
192
internal/client/request.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// paginatedResponse is the envelope returned by VirtFusion's Laravel-style pagination.
|
||||||
|
type paginatedResponse struct {
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
LastPage int `json:"last_page"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get performs a GET request to the given path.
|
||||||
|
func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error) {
|
||||||
|
return c.doRequest(ctx, http.MethodGet, path, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllPages fetches all pages from a paginated endpoint and returns
|
||||||
|
// a synthetic JSON response with all items merged into a single "data" array.
|
||||||
|
// If the response is not paginated (no last_page field or single page), it
|
||||||
|
// returns the original response unchanged.
|
||||||
|
func (c *Client) GetAllPages(ctx context.Context, path string) (json.RawMessage, error) {
|
||||||
|
firstRaw, err := c.Get(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var page paginatedResponse
|
||||||
|
if err := json.Unmarshal(firstRaw, &page); err != nil || page.LastPage == 0 {
|
||||||
|
// Not a paginated response — return as-is.
|
||||||
|
return firstRaw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.LastPage <= 1 {
|
||||||
|
return firstRaw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect data arrays from all pages.
|
||||||
|
allItems, err := flattenJSONArray(page.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing page 1 data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := "&"
|
||||||
|
if !strings.Contains(path, "?") {
|
||||||
|
sep = "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
for p := 2; p <= page.LastPage; p++ {
|
||||||
|
pageRaw, err := c.Get(ctx, fmt.Sprintf("%s%spage=%d", path, sep, p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching page %d: %w", p, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageResp paginatedResponse
|
||||||
|
if err := json.Unmarshal(pageRaw, &pageResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing page %d: %w", p, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := flattenJSONArray(pageResp.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing page %d data: %w", p, err)
|
||||||
|
}
|
||||||
|
allItems = append(allItems, items...)
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedData, err := json.Marshal(allItems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling merged data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a response that looks like {"data": [...all items...]} so
|
||||||
|
// existing list response types (e.g. ServerListResponse) unmarshal correctly.
|
||||||
|
result, err := json.Marshal(map[string]json.RawMessage{"data": mergedData})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling merged response: %w", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// flattenJSONArray unmarshals a JSON array into individual raw messages.
|
||||||
|
func flattenJSONArray(raw json.RawMessage) ([]json.RawMessage, error) {
|
||||||
|
var items []json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post performs a POST request to the given path with the given body.
|
||||||
|
func (c *Client) Post(ctx context.Context, path string, body interface{}) (json.RawMessage, error) {
|
||||||
|
return c.doRequest(ctx, http.MethodPost, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put performs a PUT request to the given path with the given body.
|
||||||
|
func (c *Client) Put(ctx context.Context, path string, body interface{}) (json.RawMessage, error) {
|
||||||
|
return c.doRequest(ctx, http.MethodPut, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete performs a DELETE request to the given path.
|
||||||
|
func (c *Client) Delete(ctx context.Context, path string) (json.RawMessage, error) {
|
||||||
|
return c.doRequest(ctx, http.MethodDelete, path, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWithBody performs a DELETE request with a JSON body.
|
||||||
|
func (c *Client) DeleteWithBody(ctx context.Context, path string, body interface{}) (json.RawMessage, error) {
|
||||||
|
return c.doRequest(ctx, http.MethodDelete, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (json.RawMessage, error) {
|
||||||
|
// Split path from query string before joining, since url.JoinPath
|
||||||
|
// escapes '?' as '%3F' when it appears in the path.
|
||||||
|
pathPart := path
|
||||||
|
queryPart := ""
|
||||||
|
if idx := strings.IndexByte(path, '?'); idx >= 0 {
|
||||||
|
pathPart = path[:idx]
|
||||||
|
queryPart = path[idx:]
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL, err := url.JoinPath(c.BaseURL, pathPart)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("building URL: %w", err)
|
||||||
|
}
|
||||||
|
fullURL += queryPart
|
||||||
|
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
jsonBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling request body: %w", err)
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(jsonBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("executing request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 204 No Content is a success with no body
|
||||||
|
if resp.StatusCode == http.StatusNoContent {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
apiErr := &APIError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Status: resp.Status,
|
||||||
|
Body: string(respBody),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse validation errors
|
||||||
|
var errResp struct {
|
||||||
|
Errors map[string][]string `json:"errors"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(respBody, &errResp) == nil && len(errResp.Errors) > 0 {
|
||||||
|
apiErr.Errors = errResp.Errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.RawMessage(respBody), nil
|
||||||
|
}
|
||||||
516
internal/client/types.go
Normal file
516
internal/client/types.go
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
// ServerCreateRequest represents the request body for creating a server.
|
||||||
|
type ServerCreateRequest struct {
|
||||||
|
PackageID int64 `json:"packageId"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
HypervisorID int64 `json:"hypervisorId"`
|
||||||
|
Ipv4 *int64 `json:"ipv4,omitempty"`
|
||||||
|
Storage *int64 `json:"storage,omitempty"`
|
||||||
|
Memory *int64 `json:"memory,omitempty"`
|
||||||
|
CPUCores *int64 `json:"cpuCores,omitempty"`
|
||||||
|
Traffic *int64 `json:"traffic,omitempty"`
|
||||||
|
NetworkSpeedInbound *int64 `json:"networkSpeedInbound,omitempty"`
|
||||||
|
NetworkSpeedOutbound *int64 `json:"networkSpeedOutbound,omitempty"`
|
||||||
|
StorageProfile *int64 `json:"storageProfile,omitempty"`
|
||||||
|
NetworkProfile *int64 `json:"networkProfile,omitempty"`
|
||||||
|
DryRun *bool `json:"dryRun,omitempty"`
|
||||||
|
AdditionalStorage1 *int64 `json:"additionalStorage1,omitempty"`
|
||||||
|
AdditionalStorage1Profile *int64 `json:"additionalStorage1Profile,omitempty"`
|
||||||
|
AdditionalStorage2 *int64 `json:"additionalStorage2,omitempty"`
|
||||||
|
AdditionalStorage2Profile *int64 `json:"additionalStorage2Profile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerResponse represents the response from the API for server operations.
|
||||||
|
type ServerResponse struct {
|
||||||
|
Data ServerData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerData represents a server in the API.
|
||||||
|
// The API returns "ownerId" (not "userId") and nests CPU/memory/storage
|
||||||
|
// under settings.resources and cpu.cores for detailed (single) responses.
|
||||||
|
// List responses use a flatter structure with "owner" as an int.
|
||||||
|
type ServerData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
OwnerID int64 `json:"ownerId"`
|
||||||
|
HypervisorID int64 `json:"hypervisorId"`
|
||||||
|
Suspended bool `json:"suspended"`
|
||||||
|
|
||||||
|
// Nested objects present in detailed (single-server) responses.
|
||||||
|
CPU *ServerCPU `json:"cpu,omitempty"`
|
||||||
|
Settings *ServerSettings `json:"settings,omitempty"`
|
||||||
|
Traffic interface{} `json:"traffic,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerCPU represents CPU info from the detailed server response.
|
||||||
|
type ServerCPU struct {
|
||||||
|
Cores int64 `json:"cores"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerSettings holds nested settings from the detailed server response.
|
||||||
|
type ServerSettings struct {
|
||||||
|
Resources *ServerSettingsResources `json:"resources,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerSettingsResources holds resource allocations from server settings.
|
||||||
|
type ServerSettingsResources struct {
|
||||||
|
Memory int64 `json:"memory"`
|
||||||
|
Storage int64 `json:"storage"`
|
||||||
|
Traffic int64 `json:"traffic"`
|
||||||
|
CPUCores int64 `json:"cpuCores"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerListResponse represents a list of servers.
|
||||||
|
type ServerListResponse struct {
|
||||||
|
Data []ServerData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerBuildRequest represents the request body for building a server.
|
||||||
|
type ServerBuildRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
OperatingSystemID int64 `json:"operatingSystemId"`
|
||||||
|
VNC bool `json:"vnc"`
|
||||||
|
Ipv6 bool `json:"ipv6"`
|
||||||
|
SSHKeys []int64 `json:"sshKeys,omitempty"`
|
||||||
|
Email bool `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyCreateRequest represents the request body for creating an SSH key.
|
||||||
|
type SSHKeyCreateRequest struct {
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyResponse represents the response from the API for SSH key operations.
|
||||||
|
type SSHKeyResponse struct {
|
||||||
|
Data SSHKeyData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyData represents an SSH key in the API.
|
||||||
|
type SSHKeyData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
CreatedAt string `json:"created"`
|
||||||
|
UpdatedAt string `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyListResponse represents a list of SSH keys.
|
||||||
|
type SSHKeyListResponse struct {
|
||||||
|
Data []SSHKeyData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCreateRequest represents the request body for creating a user.
|
||||||
|
type UserCreateRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
ExtRelationID string `json:"extRelationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResponse represents the response from the API for user operations.
|
||||||
|
type UserResponse struct {
|
||||||
|
Data UserData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserData represents a user in the API.
|
||||||
|
type UserData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
ExtRelationID string `json:"extRelationId"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt string `json:"created"`
|
||||||
|
UpdatedAt string `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserModifyRequest represents the request body for modifying a user.
|
||||||
|
type UserModifyRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorResponse represents the response from the API for hypervisor operations.
|
||||||
|
type HypervisorResponse struct {
|
||||||
|
Data HypervisorData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorData represents a hypervisor in the API.
|
||||||
|
type HypervisorData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorListResponse represents a list of hypervisors.
|
||||||
|
type HypervisorListResponse struct {
|
||||||
|
Data []HypervisorData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupResponse represents the response from the API for hypervisor group operations.
|
||||||
|
type HypervisorGroupResponse struct {
|
||||||
|
Data HypervisorGroupData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupData represents a hypervisor group in the API.
|
||||||
|
type HypervisorGroupData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupListResponse represents a list of hypervisor groups.
|
||||||
|
type HypervisorGroupListResponse struct {
|
||||||
|
Data []HypervisorGroupData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageResponse represents the response from the API for package operations.
|
||||||
|
type PackageResponse struct {
|
||||||
|
Data PackageData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageData represents a package in the API.
|
||||||
|
// The API uses "primaryStorage", "primaryNetworkSpeedIn/Out" etc.
|
||||||
|
type PackageData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CPUCores int64 `json:"cpuCores"`
|
||||||
|
Memory int64 `json:"memory"`
|
||||||
|
Storage int64 `json:"primaryStorage"`
|
||||||
|
Traffic int64 `json:"traffic"`
|
||||||
|
NetworkSpeedInbound int64 `json:"primaryNetworkSpeedIn"`
|
||||||
|
NetworkSpeedOutbound int64 `json:"primaryNetworkSpeedOut"`
|
||||||
|
Ipv4 int64 `json:"ipv4"`
|
||||||
|
StorageProfile int64 `json:"primaryStorageProfile"`
|
||||||
|
NetworkProfile int64 `json:"primaryNetworkProfile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageListResponse represents a list of packages.
|
||||||
|
type PackageListResponse struct {
|
||||||
|
Data []PackageData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockResponse represents the response from the API for IP block operations.
|
||||||
|
type IPBlockResponse struct {
|
||||||
|
Data IPBlockData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockData represents an IP block in the API.
|
||||||
|
// The API nests gateway/netmask under "ipv4" and returns "type" as an int.
|
||||||
|
type IPBlockData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type int64 `json:"type"`
|
||||||
|
IPv4 IPBlockIPv4 `json:"ipv4"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockIPv4 represents the IPv4 section of an IP block.
|
||||||
|
type IPBlockIPv4 struct {
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
Netmask string `json:"netmask"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockListResponse represents a list of IP blocks.
|
||||||
|
type IPBlockListResponse struct {
|
||||||
|
Data []IPBlockData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirewallResponse represents the response from the API for firewall operations.
|
||||||
|
type FirewallResponse struct {
|
||||||
|
Data FirewallData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirewallData represents firewall data in the API.
|
||||||
|
type FirewallData struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Rules []FirewallRule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirewallRule represents a single firewall rule.
|
||||||
|
type FirewallRule struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirewallSetRulesRequest represents the request body for setting firewall rules.
|
||||||
|
type FirewallSetRulesRequest struct {
|
||||||
|
Rules []FirewallRule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkWhitelistRequest represents the request body for adding a network whitelist entry.
|
||||||
|
type NetworkWhitelistRequest struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficBlockRequest represents the request body for adding a traffic block.
|
||||||
|
type TrafficBlockRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficBlockResponse represents the response for traffic block operations.
|
||||||
|
type TrafficBlockResponse struct {
|
||||||
|
Data TrafficBlockData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficBlockData represents a traffic block.
|
||||||
|
type TrafficBlockData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficBlockListResponse represents a list of traffic blocks.
|
||||||
|
type TrafficBlockListResponse struct {
|
||||||
|
Data []TrafficBlockData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficResponse represents server traffic data.
|
||||||
|
type TrafficResponse struct {
|
||||||
|
Data TrafficData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrafficData represents traffic usage data.
|
||||||
|
type TrafficData struct {
|
||||||
|
Used int64 `json:"used"`
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockRangeRequest represents the request body for adding an IPv4 range.
|
||||||
|
type IPBlockRangeRequest struct {
|
||||||
|
StartIP string `json:"startIp"`
|
||||||
|
EndIP string `json:"endIp"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
Netmask string `json:"netmask"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerModifyNameRequest represents the request body for modifying a server name.
|
||||||
|
type ServerModifyNameRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerModifyCPURequest represents the request body for modifying server CPU.
|
||||||
|
type ServerModifyCPURequest struct {
|
||||||
|
CPUCores int64 `json:"cpuCores"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerModifyMemoryRequest represents the request body for modifying server memory.
|
||||||
|
type ServerModifyMemoryRequest struct {
|
||||||
|
Memory int64 `json:"memory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerModifyTrafficRequest represents the request body for modifying server traffic.
|
||||||
|
type ServerModifyTrafficRequest struct {
|
||||||
|
Traffic int64 `json:"traffic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerModifyCPUThrottleRequest represents the request body for modifying CPU throttle.
|
||||||
|
type ServerModifyCPUThrottleRequest struct {
|
||||||
|
Percentage int64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerCustomXMLRequest represents the request body for setting custom XML.
|
||||||
|
type ServerCustomXMLRequest struct {
|
||||||
|
XML string `json:"xml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VNCResponse represents the response for VNC operations.
|
||||||
|
type VNCResponse struct {
|
||||||
|
Data VNCData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VNCData represents VNC connection data.
|
||||||
|
type VNCData struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupResponse represents the response for backup operations.
|
||||||
|
type BackupResponse struct {
|
||||||
|
Data []BackupData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupData represents a backup entry.
|
||||||
|
type BackupData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
CreatedAt string `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSServiceResponse represents the response for DNS service operations.
|
||||||
|
type DNSServiceResponse struct {
|
||||||
|
Data DNSServiceData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSServiceData represents a DNS service.
|
||||||
|
type DNSServiceData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISOResponse represents the response for ISO operations.
|
||||||
|
type ISOResponse struct {
|
||||||
|
Data ISOData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISOData represents an ISO.
|
||||||
|
type ISOData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueResponse represents the response for queue operations.
|
||||||
|
type QueueResponse struct {
|
||||||
|
Data QueueData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueData represents a queue item.
|
||||||
|
type QueueData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
CreatedAt string `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateResponse represents the response for template operations.
|
||||||
|
type TemplateResponse struct {
|
||||||
|
Data []TemplateData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateData represents a template.
|
||||||
|
type TemplateData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCreditRequest represents the request body for adding credit.
|
||||||
|
type SelfServiceCreditRequest struct {
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CurrencyCode string `json:"currencyCode"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCreditResponse represents the response for credit operations.
|
||||||
|
type SelfServiceCreditResponse struct {
|
||||||
|
Data SelfServiceCreditData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCreditData represents credit data.
|
||||||
|
type SelfServiceCreditData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackRequest represents the request body for resource packs.
|
||||||
|
type SelfServiceResourcePackRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
PackID int64 `json:"packId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackResponse represents the response for resource pack operations.
|
||||||
|
type SelfServiceResourcePackResponse struct {
|
||||||
|
Data SelfServiceResourcePackData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackData represents a resource pack.
|
||||||
|
type SelfServiceResourcePackData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
PackID int64 `json:"packId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrencyResponse represents the response for currency operations.
|
||||||
|
type CurrencyResponse struct {
|
||||||
|
Data []CurrencyData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrencyData represents a currency.
|
||||||
|
type CurrencyData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerIPv4AddRequest represents the request body for adding IPv4 to a server.
|
||||||
|
type ServerIPv4AddRequest struct {
|
||||||
|
Quantity int64 `json:"quantity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthTokenResponse represents the response for auth token generation.
|
||||||
|
type AuthTokenResponse struct {
|
||||||
|
Data AuthTokenData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthTokenData represents an auth token.
|
||||||
|
type AuthTokenData struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordResetResponse represents the response for password reset operations.
|
||||||
|
type PasswordResetResponse struct {
|
||||||
|
Data PasswordResetData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordResetData represents password reset data.
|
||||||
|
type PasswordResetData struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupResourcesResponse represents the response for hypervisor group resources.
|
||||||
|
// The API returns a paginated array of per-hypervisor resource entries.
|
||||||
|
type HypervisorGroupResourcesResponse struct {
|
||||||
|
Data []HypervisorGroupResourceEntry `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupResourceEntry represents a single hypervisor's resources within a group.
|
||||||
|
type HypervisorGroupResourceEntry struct {
|
||||||
|
Hypervisor HypervisorData `json:"hypervisor"`
|
||||||
|
Resources HypervisorGroupResourceDetail `json:"resources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupResourceDetail represents the resource metrics for a hypervisor.
|
||||||
|
type HypervisorGroupResourceDetail struct {
|
||||||
|
Memory ResourceMetric `json:"memory"`
|
||||||
|
CPUCores ResourceMetric `json:"cpuCores"`
|
||||||
|
LocalStorage ResourceMetric `json:"localStorage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMetric represents a resource metric with max/allocated/free values.
|
||||||
|
type ResourceMetric struct {
|
||||||
|
Max int64 `json:"max"`
|
||||||
|
Allocated int64 `json:"allocated"`
|
||||||
|
Free int64 `json:"free"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyStatsResponse represents the response for hourly stats.
|
||||||
|
type SelfServiceHourlyStatsResponse struct {
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceReportResponse represents the response for self-service reports.
|
||||||
|
type SelfServiceReportResponse struct {
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceUsageResponse represents the response for self-service usage.
|
||||||
|
type SelfServiceUsageResponse struct {
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
105
internal/provider/data_source_dns_service.go
Normal file
105
internal/provider/data_source_dns_service.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &DNSServiceDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &DNSServiceDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDNSServiceDataSource returns a new DNS service data source.
|
||||||
|
func NewDNSServiceDataSource() datasource.DataSource {
|
||||||
|
return &DNSServiceDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSServiceDataSource defines the data source implementation.
|
||||||
|
type DNSServiceDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSServiceDataSourceModel describes the data source data model.
|
||||||
|
type DNSServiceDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSServiceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_dns_service"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSServiceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion DNS service by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The DNS service ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The DNS service name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The DNS service type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSServiceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSServiceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data DNSServiceDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/dns/services/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading DNS Service", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dnsResp client.DNSServiceResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &dnsResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing DNS Service Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(dnsResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(dnsResp.Data.Name)
|
||||||
|
data.Type = types.StringValue(dnsResp.Data.Type)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
117
internal/provider/data_source_hypervisor.go
Normal file
117
internal/provider/data_source_hypervisor.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &HypervisorDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &HypervisorDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHypervisorDataSource returns a new hypervisor data source.
|
||||||
|
func NewHypervisorDataSource() datasource.DataSource {
|
||||||
|
return &HypervisorDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorDataSource defines the data source implementation.
|
||||||
|
type HypervisorDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorDataSourceModel describes the data source data model.
|
||||||
|
type HypervisorDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
Hostname types.String `tfsdk:"hostname"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_hypervisor"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion hypervisor by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"hostname": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor hostname.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the hypervisor is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data HypervisorDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/compute/hypervisors/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Hypervisor", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var hypervisorResp client.HypervisorResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &hypervisorResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Hypervisor Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(hypervisorResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(hypervisorResp.Data.Name)
|
||||||
|
data.Type = types.StringValue(hypervisorResp.Data.Type)
|
||||||
|
data.Hostname = types.StringValue(hypervisorResp.Data.Hostname)
|
||||||
|
data.Enabled = types.BoolValue(hypervisorResp.Data.Enabled)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
105
internal/provider/data_source_hypervisor_group.go
Normal file
105
internal/provider/data_source_hypervisor_group.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &HypervisorGroupDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &HypervisorGroupDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHypervisorGroupDataSource returns a new hypervisor group data source.
|
||||||
|
func NewHypervisorGroupDataSource() datasource.DataSource {
|
||||||
|
return &HypervisorGroupDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupDataSource defines the data source implementation.
|
||||||
|
type HypervisorGroupDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupDataSourceModel describes the data source data model.
|
||||||
|
type HypervisorGroupDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_hypervisor_group"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion hypervisor group by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor group ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor group name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the hypervisor group is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data HypervisorGroupDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/compute/hypervisors/groups/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Hypervisor Group", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupResp client.HypervisorGroupResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &groupResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Hypervisor Group Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(groupResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(groupResp.Data.Name)
|
||||||
|
data.Enabled = types.BoolValue(groupResp.Data.Enabled)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
119
internal/provider/data_source_hypervisor_group_resources.go
Normal file
119
internal/provider/data_source_hypervisor_group_resources.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &HypervisorGroupResourcesDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &HypervisorGroupResourcesDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHypervisorGroupResourcesDataSource returns a new hypervisor group resources data source.
|
||||||
|
func NewHypervisorGroupResourcesDataSource() datasource.DataSource {
|
||||||
|
return &HypervisorGroupResourcesDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupResourcesDataSource defines the data source implementation.
|
||||||
|
type HypervisorGroupResourcesDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupResourcesDataSourceModel describes the data source data model.
|
||||||
|
type HypervisorGroupResourcesDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
CPUCores types.Int64 `tfsdk:"cpu_cores"`
|
||||||
|
Memory types.Int64 `tfsdk:"memory"`
|
||||||
|
Storage types.Int64 `tfsdk:"storage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupResourcesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_hypervisor_group_resources"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupResourcesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches resource information for a VirtFusion hypervisor group.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor group ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"cpu_cores": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of CPU cores available in the group.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"memory": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of memory available in the group.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"storage": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of storage available in the group.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupResourcesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupResourcesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data HypervisorGroupResourcesDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors/groups/%d/resources?%s", data.ID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Hypervisor Group Resources", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourcesResp client.HypervisorGroupResourcesResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &resourcesResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Hypervisor Group Resources Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate resource totals across all hypervisors in the group.
|
||||||
|
var totalCPU, totalMemory, totalStorage int64
|
||||||
|
for _, entry := range resourcesResp.Data {
|
||||||
|
totalCPU += entry.Resources.CPUCores.Max
|
||||||
|
totalMemory += entry.Resources.Memory.Max
|
||||||
|
totalStorage += entry.Resources.LocalStorage.Max
|
||||||
|
}
|
||||||
|
data.CPUCores = types.Int64Value(totalCPU)
|
||||||
|
data.Memory = types.Int64Value(totalMemory)
|
||||||
|
data.Storage = types.Int64Value(totalStorage)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
121
internal/provider/data_source_hypervisor_groups.go
Normal file
121
internal/provider/data_source_hypervisor_groups.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &HypervisorGroupsDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &HypervisorGroupsDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHypervisorGroupsDataSource returns a new hypervisor groups data source.
|
||||||
|
func NewHypervisorGroupsDataSource() datasource.DataSource {
|
||||||
|
return &HypervisorGroupsDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupsDataSource defines the data source implementation.
|
||||||
|
type HypervisorGroupsDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupsDataSourceModel describes the data source data model.
|
||||||
|
type HypervisorGroupsDataSourceModel struct {
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Groups []HypervisorGroupItemModel `tfsdk:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorGroupItemModel describes a single hypervisor group in the list.
|
||||||
|
type HypervisorGroupItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_hypervisor_groups"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches all VirtFusion hypervisor groups.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"groups": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of hypervisor groups.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor group ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor group name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the hypervisor group is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorGroupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data HypervisorGroupsDataSourceModel
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors/groups?%s", resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Hypervisor Groups", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.HypervisorGroupListResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Hypervisor Groups Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Groups = make([]HypervisorGroupItemModel, len(listResp.Data))
|
||||||
|
for i, g := range listResp.Data {
|
||||||
|
data.Groups[i] = HypervisorGroupItemModel{
|
||||||
|
ID: types.Int64Value(g.ID),
|
||||||
|
Name: types.StringValue(g.Name),
|
||||||
|
Enabled: types.BoolValue(g.Enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
133
internal/provider/data_source_hypervisors.go
Normal file
133
internal/provider/data_source_hypervisors.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &HypervisorsDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &HypervisorsDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHypervisorsDataSource returns a new hypervisors data source.
|
||||||
|
func NewHypervisorsDataSource() datasource.DataSource {
|
||||||
|
return &HypervisorsDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorsDataSource defines the data source implementation.
|
||||||
|
type HypervisorsDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorsDataSourceModel describes the data source data model.
|
||||||
|
type HypervisorsDataSourceModel struct {
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Hypervisors []HypervisorItemModel `tfsdk:"hypervisors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HypervisorItemModel describes a single hypervisor in the list.
|
||||||
|
type HypervisorItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
Hostname types.String `tfsdk:"hostname"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_hypervisors"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches all VirtFusion hypervisors.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"hypervisors": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of hypervisors.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"hostname": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hypervisor hostname.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the hypervisor is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HypervisorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data HypervisorsDataSourceModel
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors?%s", resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Hypervisors", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.HypervisorListResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Hypervisors Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Hypervisors = make([]HypervisorItemModel, len(listResp.Data))
|
||||||
|
for i, h := range listResp.Data {
|
||||||
|
data.Hypervisors[i] = HypervisorItemModel{
|
||||||
|
ID: types.Int64Value(h.ID),
|
||||||
|
Name: types.StringValue(h.Name),
|
||||||
|
Type: types.StringValue(h.Type),
|
||||||
|
Hostname: types.StringValue(h.Hostname),
|
||||||
|
Enabled: types.BoolValue(h.Enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
123
internal/provider/data_source_ip_block.go
Normal file
123
internal/provider/data_source_ip_block.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &IPBlockDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &IPBlockDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewIPBlockDataSource returns a new IP block data source.
|
||||||
|
func NewIPBlockDataSource() datasource.DataSource {
|
||||||
|
return &IPBlockDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockDataSource defines the data source implementation.
|
||||||
|
type IPBlockDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockDataSourceModel describes the data source data model.
|
||||||
|
type IPBlockDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.Int64 `tfsdk:"type"`
|
||||||
|
Gateway types.String `tfsdk:"gateway"`
|
||||||
|
Netmask types.String `tfsdk:"netmask"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlockDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_ip_block"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlockDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion IP block by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The IP block ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IP block name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The IP block type (4 = IPv4, 6 = IPv6).",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"gateway": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IPv4 gateway address.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"netmask": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IPv4 netmask.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the IP block is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlockDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlockDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data IPBlockDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/connectivity/ipblocks/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading IP Block", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockResp client.IPBlockResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &blockResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing IP Block Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(blockResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(blockResp.Data.Name)
|
||||||
|
data.Type = types.Int64Value(blockResp.Data.Type)
|
||||||
|
data.Gateway = types.StringValue(blockResp.Data.IPv4.Gateway)
|
||||||
|
data.Netmask = types.StringValue(blockResp.Data.IPv4.Netmask)
|
||||||
|
data.Enabled = types.BoolValue(blockResp.Data.Enabled)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
139
internal/provider/data_source_ip_blocks.go
Normal file
139
internal/provider/data_source_ip_blocks.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &IPBlocksDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &IPBlocksDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewIPBlocksDataSource returns a new IP blocks data source.
|
||||||
|
func NewIPBlocksDataSource() datasource.DataSource {
|
||||||
|
return &IPBlocksDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlocksDataSource defines the data source implementation.
|
||||||
|
type IPBlocksDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlocksDataSourceModel describes the data source data model.
|
||||||
|
type IPBlocksDataSourceModel struct {
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
IPBlocks []IPBlockItemModel `tfsdk:"ip_blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockItemModel describes a single IP block in the list.
|
||||||
|
type IPBlockItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.Int64 `tfsdk:"type"`
|
||||||
|
Gateway types.String `tfsdk:"gateway"`
|
||||||
|
Netmask types.String `tfsdk:"netmask"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlocksDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_ip_blocks"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlocksDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches all VirtFusion IP blocks.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"ip_blocks": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of IP blocks.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The IP block ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IP block name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The IP block type (4 = IPv4, 6 = IPv6).",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"gateway": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IPv4 gateway address.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"netmask": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IPv4 netmask.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the IP block is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlocksDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IPBlocksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data IPBlocksDataSourceModel
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/connectivity/ipblocks?%s", resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading IP Blocks", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.IPBlockListResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing IP Blocks Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.IPBlocks = make([]IPBlockItemModel, len(listResp.Data))
|
||||||
|
for i, b := range listResp.Data {
|
||||||
|
data.IPBlocks[i] = IPBlockItemModel{
|
||||||
|
ID: types.Int64Value(b.ID),
|
||||||
|
Name: types.StringValue(b.Name),
|
||||||
|
Type: types.Int64Value(b.Type),
|
||||||
|
Gateway: types.StringValue(b.IPv4.Gateway),
|
||||||
|
Netmask: types.StringValue(b.IPv4.Netmask),
|
||||||
|
Enabled: types.BoolValue(b.Enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
99
internal/provider/data_source_iso.go
Normal file
99
internal/provider/data_source_iso.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &ISODataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &ISODataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewISODataSource returns a new ISO data source.
|
||||||
|
func NewISODataSource() datasource.DataSource {
|
||||||
|
return &ISODataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISODataSource defines the data source implementation.
|
||||||
|
type ISODataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISODataSourceModel describes the data source data model.
|
||||||
|
type ISODataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ISODataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_iso"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ISODataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion ISO by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ISO ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The ISO name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ISODataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ISODataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ISODataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/media/iso/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading ISO", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var isoResp client.ISOResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &isoResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing ISO Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(isoResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(isoResp.Data.Name)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
147
internal/provider/data_source_package.go
Normal file
147
internal/provider/data_source_package.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &PackageDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &PackageDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPackageDataSource returns a new package data source.
|
||||||
|
func NewPackageDataSource() datasource.DataSource {
|
||||||
|
return &PackageDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageDataSource defines the data source implementation.
|
||||||
|
type PackageDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageDataSourceModel describes the data source data model.
|
||||||
|
type PackageDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
CPUCores types.Int64 `tfsdk:"cpu_cores"`
|
||||||
|
Memory types.Int64 `tfsdk:"memory"`
|
||||||
|
Storage types.Int64 `tfsdk:"storage"`
|
||||||
|
Traffic types.Int64 `tfsdk:"traffic"`
|
||||||
|
NetworkSpeedInbound types.Int64 `tfsdk:"network_speed_inbound"`
|
||||||
|
NetworkSpeedOutbound types.Int64 `tfsdk:"network_speed_outbound"`
|
||||||
|
Ipv4 types.Int64 `tfsdk:"ipv4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_package"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion package by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The package ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The package name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the package is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"cpu_cores": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of CPU cores in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"memory": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of memory in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"storage": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of storage in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"traffic": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The traffic limit in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"network_speed_inbound": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The inbound network speed in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"network_speed_outbound": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The outbound network speed in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"ipv4": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of IPv4 addresses in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data PackageDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/packages/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Package", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var pkgResp client.PackageResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &pkgResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Package Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(pkgResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(pkgResp.Data.Name)
|
||||||
|
data.Enabled = types.BoolValue(pkgResp.Data.Enabled)
|
||||||
|
data.CPUCores = types.Int64Value(pkgResp.Data.CPUCores)
|
||||||
|
data.Memory = types.Int64Value(pkgResp.Data.Memory)
|
||||||
|
data.Storage = types.Int64Value(pkgResp.Data.Storage)
|
||||||
|
data.Traffic = types.Int64Value(pkgResp.Data.Traffic)
|
||||||
|
data.NetworkSpeedInbound = types.Int64Value(pkgResp.Data.NetworkSpeedInbound)
|
||||||
|
data.NetworkSpeedOutbound = types.Int64Value(pkgResp.Data.NetworkSpeedOutbound)
|
||||||
|
data.Ipv4 = types.Int64Value(pkgResp.Data.Ipv4)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
124
internal/provider/data_source_package_templates.go
Normal file
124
internal/provider/data_source_package_templates.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &PackageTemplatesDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &PackageTemplatesDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPackageTemplatesDataSource returns a new package templates data source.
|
||||||
|
func NewPackageTemplatesDataSource() datasource.DataSource {
|
||||||
|
return &PackageTemplatesDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageTemplatesDataSource defines the data source implementation.
|
||||||
|
type PackageTemplatesDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageTemplatesDataSourceModel describes the data source data model.
|
||||||
|
type PackageTemplatesDataSourceModel struct {
|
||||||
|
PackageID types.Int64 `tfsdk:"package_id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Templates []PackageTemplateItemModel `tfsdk:"templates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageTemplateItemModel describes a single template in the list.
|
||||||
|
type PackageTemplateItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageTemplatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_package_templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageTemplatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches templates available for a VirtFusion server package.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"package_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The package ID to fetch templates for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"templates": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of templates available for the package.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The template ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The template name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageTemplatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackageTemplatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data PackageTemplatesDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/media/templates/fromServerPackageSpec/%d?%s", data.PackageID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Package Templates", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateResp client.TemplateResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &templateResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Package Templates Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Templates = make([]PackageTemplateItemModel, len(templateResp.Data))
|
||||||
|
for i, t := range templateResp.Data {
|
||||||
|
data.Templates[i] = PackageTemplateItemModel{
|
||||||
|
ID: types.Int64Value(t.ID),
|
||||||
|
Name: types.StringValue(t.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
163
internal/provider/data_source_packages.go
Normal file
163
internal/provider/data_source_packages.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &PackagesDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &PackagesDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPackagesDataSource returns a new packages data source.
|
||||||
|
func NewPackagesDataSource() datasource.DataSource {
|
||||||
|
return &PackagesDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackagesDataSource defines the data source implementation.
|
||||||
|
type PackagesDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackagesDataSourceModel describes the data source data model.
|
||||||
|
type PackagesDataSourceModel struct {
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Packages []PackageItemModel `tfsdk:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageItemModel describes a single package in the list.
|
||||||
|
type PackageItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
CPUCores types.Int64 `tfsdk:"cpu_cores"`
|
||||||
|
Memory types.Int64 `tfsdk:"memory"`
|
||||||
|
Storage types.Int64 `tfsdk:"storage"`
|
||||||
|
Traffic types.Int64 `tfsdk:"traffic"`
|
||||||
|
NetworkSpeedInbound types.Int64 `tfsdk:"network_speed_inbound"`
|
||||||
|
NetworkSpeedOutbound types.Int64 `tfsdk:"network_speed_outbound"`
|
||||||
|
Ipv4 types.Int64 `tfsdk:"ipv4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackagesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_packages"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackagesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches all VirtFusion packages.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"packages": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of packages.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The package ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The package name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the package is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"cpu_cores": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of CPU cores in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"memory": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of memory in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"storage": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of storage in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"traffic": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The traffic limit in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"network_speed_inbound": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The inbound network speed in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"network_speed_outbound": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The outbound network speed in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"ipv4": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of IPv4 addresses in the package.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackagesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PackagesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data PackagesDataSourceModel
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/packages?%s", resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Packages", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.PackageListResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Packages Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Packages = make([]PackageItemModel, len(listResp.Data))
|
||||||
|
for i, p := range listResp.Data {
|
||||||
|
data.Packages[i] = PackageItemModel{
|
||||||
|
ID: types.Int64Value(p.ID),
|
||||||
|
Name: types.StringValue(p.Name),
|
||||||
|
Enabled: types.BoolValue(p.Enabled),
|
||||||
|
CPUCores: types.Int64Value(p.CPUCores),
|
||||||
|
Memory: types.Int64Value(p.Memory),
|
||||||
|
Storage: types.Int64Value(p.Storage),
|
||||||
|
Traffic: types.Int64Value(p.Traffic),
|
||||||
|
NetworkSpeedInbound: types.Int64Value(p.NetworkSpeedInbound),
|
||||||
|
NetworkSpeedOutbound: types.Int64Value(p.NetworkSpeedOutbound),
|
||||||
|
Ipv4: types.Int64Value(p.Ipv4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
111
internal/provider/data_source_queue_item.go
Normal file
111
internal/provider/data_source_queue_item.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &QueueItemDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &QueueItemDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewQueueItemDataSource returns a new queue item data source.
|
||||||
|
func NewQueueItemDataSource() datasource.DataSource {
|
||||||
|
return &QueueItemDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueItemDataSource defines the data source implementation.
|
||||||
|
type QueueItemDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueItemDataSourceModel describes the data source data model.
|
||||||
|
type QueueItemDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Status types.String `tfsdk:"status"`
|
||||||
|
Action types.String `tfsdk:"action"`
|
||||||
|
CreatedAt types.String `tfsdk:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QueueItemDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_queue_item"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QueueItemDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion queue item by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The queue item ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"status": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The queue item status.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"action": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The queue item action.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"created_at": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The creation timestamp.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QueueItemDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QueueItemDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data QueueItemDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/queue/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Queue Item", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var queueResp client.QueueResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &queueResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Queue Item Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(queueResp.Data.ID)
|
||||||
|
data.Status = types.StringValue(queueResp.Data.Status)
|
||||||
|
data.Action = types.StringValue(queueResp.Data.Action)
|
||||||
|
data.CreatedAt = types.StringValue(queueResp.Data.CreatedAt)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
121
internal/provider/data_source_self_service_currencies.go
Normal file
121
internal/provider/data_source_self_service_currencies.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SelfServiceCurrenciesDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SelfServiceCurrenciesDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceCurrenciesDataSource returns a new self-service currencies data source.
|
||||||
|
func NewSelfServiceCurrenciesDataSource() datasource.DataSource {
|
||||||
|
return &SelfServiceCurrenciesDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCurrenciesDataSource defines the data source implementation.
|
||||||
|
type SelfServiceCurrenciesDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCurrenciesDataSourceModel describes the data source data model.
|
||||||
|
type SelfServiceCurrenciesDataSourceModel struct {
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Currencies []CurrencyItemModel `tfsdk:"currencies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrencyItemModel describes a single currency in the list.
|
||||||
|
type CurrencyItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Code types.String `tfsdk:"code"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceCurrenciesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_currencies"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceCurrenciesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches all available VirtFusion self-service currencies.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"currencies": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of available currencies.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The currency ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"code": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The currency code.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The currency name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceCurrenciesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceCurrenciesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SelfServiceCurrenciesDataSourceModel
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/selfService/currencies?%s", resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Self-Service Currencies", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var currencyResp client.CurrencyResponse
|
||||||
|
if err := json.Unmarshal(rawResp, ¤cyResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Self-Service Currencies Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Currencies = make([]CurrencyItemModel, len(currencyResp.Data))
|
||||||
|
for i, c := range currencyResp.Data {
|
||||||
|
data.Currencies[i] = CurrencyItemModel{
|
||||||
|
ID: types.Int64Value(c.ID),
|
||||||
|
Code: types.StringValue(c.Code),
|
||||||
|
Name: types.StringValue(c.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
96
internal/provider/data_source_self_service_hourly_stats.go
Normal file
96
internal/provider/data_source_self_service_hourly_stats.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SelfServiceHourlyStatsDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SelfServiceHourlyStatsDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceHourlyStatsDataSource returns a new self-service hourly stats data source.
|
||||||
|
func NewSelfServiceHourlyStatsDataSource() datasource.DataSource {
|
||||||
|
return &SelfServiceHourlyStatsDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyStatsDataSource defines the data source implementation.
|
||||||
|
type SelfServiceHourlyStatsDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyStatsDataSourceModel describes the data source data model.
|
||||||
|
type SelfServiceHourlyStatsDataSourceModel struct {
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
GroupID types.Int64 `tfsdk:"group_id"`
|
||||||
|
StatsJSON types.String `tfsdk:"stats_json"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceHourlyStatsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_hourly_stats"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceHourlyStatsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches VirtFusion self-service hourly stats for a user and group.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID to fetch hourly stats for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"group_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The group ID to fetch hourly stats for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"stats_json": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The raw JSON response containing hourly stats.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceHourlyStatsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceHourlyStatsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SelfServiceHourlyStatsDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/hourlyStats/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Self-Service Hourly Stats", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.StatsJSON = types.StringValue(string(rawResp))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
96
internal/provider/data_source_self_service_report.go
Normal file
96
internal/provider/data_source_self_service_report.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SelfServiceReportDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SelfServiceReportDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceReportDataSource returns a new self-service report data source.
|
||||||
|
func NewSelfServiceReportDataSource() datasource.DataSource {
|
||||||
|
return &SelfServiceReportDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceReportDataSource defines the data source implementation.
|
||||||
|
type SelfServiceReportDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceReportDataSourceModel describes the data source data model.
|
||||||
|
type SelfServiceReportDataSourceModel struct {
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
GroupID types.Int64 `tfsdk:"group_id"`
|
||||||
|
ReportJSON types.String `tfsdk:"report_json"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceReportDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_report"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceReportDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a VirtFusion self-service report for a user and group.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID to fetch the report for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"group_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The group ID to fetch the report for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"report_json": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The raw JSON response containing the report.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceReportDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceReportDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SelfServiceReportDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/report/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Self-Service Report", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ReportJSON = types.StringValue(string(rawResp))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
111
internal/provider/data_source_self_service_resource_pack.go
Normal file
111
internal/provider/data_source_self_service_resource_pack.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SelfServiceResourcePackDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SelfServiceResourcePackDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceResourcePackDataSource returns a new self-service resource pack data source.
|
||||||
|
func NewSelfServiceResourcePackDataSource() datasource.DataSource {
|
||||||
|
return &SelfServiceResourcePackDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackDataSource defines the data source implementation.
|
||||||
|
type SelfServiceResourcePackDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackDataSourceModel describes the data source data model.
|
||||||
|
type SelfServiceResourcePackDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
PackID types.Int64 `tfsdk:"pack_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceResourcePackDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_resource_pack"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceResourcePackDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion self-service resource pack by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The resource pack ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The resource pack name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID associated with the resource pack.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"pack_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The pack ID associated with the resource pack.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceResourcePackDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceResourcePackDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SelfServiceResourcePackDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Self-Service Resource Pack", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var packResp client.SelfServiceResourcePackResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &packResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Self-Service Resource Pack Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(packResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(packResp.Data.Name)
|
||||||
|
data.UserID = types.Int64Value(packResp.Data.UserID)
|
||||||
|
data.PackID = types.Int64Value(packResp.Data.PackID)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
96
internal/provider/data_source_self_service_usage.go
Normal file
96
internal/provider/data_source_self_service_usage.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SelfServiceUsageDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SelfServiceUsageDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceUsageDataSource returns a new self-service usage data source.
|
||||||
|
func NewSelfServiceUsageDataSource() datasource.DataSource {
|
||||||
|
return &SelfServiceUsageDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceUsageDataSource defines the data source implementation.
|
||||||
|
type SelfServiceUsageDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceUsageDataSourceModel describes the data source data model.
|
||||||
|
type SelfServiceUsageDataSourceModel struct {
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
GroupID types.Int64 `tfsdk:"group_id"`
|
||||||
|
UsageJSON types.String `tfsdk:"usage_json"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceUsageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_usage"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceUsageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches VirtFusion self-service usage data for a user and group.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID to fetch usage data for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"group_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The group ID to fetch usage data for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"usage_json": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The raw JSON response containing usage data.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceUsageDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SelfServiceUsageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SelfServiceUsageDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/usage/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Self-Service Usage", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.UsageJSON = types.StringValue(string(rawResp))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
156
internal/provider/data_source_server.go
Normal file
156
internal/provider/data_source_server.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerDataSource{}
|
||||||
|
|
||||||
|
// NewServerDataSource returns a new data source for reading a single server.
|
||||||
|
func NewServerDataSource() datasource.DataSource {
|
||||||
|
return &ServerDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerDataSource defines the data source implementation.
|
||||||
|
type ServerDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerDataSourceModel describes the data source data model.
|
||||||
|
type ServerDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
UUID types.String `tfsdk:"uuid"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Hostname types.String `tfsdk:"hostname"`
|
||||||
|
OwnerID types.Int64 `tfsdk:"owner_id"`
|
||||||
|
HypervisorID types.Int64 `tfsdk:"hypervisor_id"`
|
||||||
|
Suspended types.Bool `tfsdk:"suspended"`
|
||||||
|
CPUCores types.Int64 `tfsdk:"cpu_cores"`
|
||||||
|
Memory types.Int64 `tfsdk:"memory"`
|
||||||
|
Storage types.Int64 `tfsdk:"storage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to read a single VirtFusion server by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"uuid": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server UUID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server display name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"hostname": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server hostname.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"owner_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The owner (user) ID who owns the server.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"hypervisor_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor ID where the server is hosted.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"suspended": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the server is suspended.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"cpu_cores": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of CPU cores.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"memory": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The memory size in MB.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"storage": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The storage size in GB.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverResp client.ServerResponse
|
||||||
|
if err := json.Unmarshal(result, &serverResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := serverResp.Data
|
||||||
|
data.ID = types.Int64Value(s.ID)
|
||||||
|
data.UUID = types.StringValue(s.UUID)
|
||||||
|
data.Name = types.StringValue(s.Name)
|
||||||
|
data.Hostname = types.StringValue(s.Hostname)
|
||||||
|
data.OwnerID = types.Int64Value(s.OwnerID)
|
||||||
|
data.HypervisorID = types.Int64Value(s.HypervisorID)
|
||||||
|
data.Suspended = types.BoolValue(s.Suspended)
|
||||||
|
|
||||||
|
// Extract nested resource values from the detailed server response.
|
||||||
|
var cpuCores, memory, storage int64
|
||||||
|
if s.CPU != nil {
|
||||||
|
cpuCores = s.CPU.Cores
|
||||||
|
}
|
||||||
|
if s.Settings != nil && s.Settings.Resources != nil {
|
||||||
|
memory = s.Settings.Resources.Memory
|
||||||
|
storage = s.Settings.Resources.Storage
|
||||||
|
}
|
||||||
|
data.CPUCores = types.Int64Value(cpuCores)
|
||||||
|
data.Memory = types.Int64Value(memory)
|
||||||
|
data.Storage = types.Int64Value(storage)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
144
internal/provider/data_source_server_backups.go
Normal file
144
internal/provider/data_source_server_backups.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerBackupsDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerBackupsDataSource{}
|
||||||
|
|
||||||
|
// NewServerBackupsDataSource returns a new data source for listing server backups.
|
||||||
|
func NewServerBackupsDataSource() datasource.DataSource {
|
||||||
|
return &ServerBackupsDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerBackupsDataSource defines the data source implementation.
|
||||||
|
type ServerBackupsDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerBackupsDataSourceModel describes the data source data model.
|
||||||
|
type ServerBackupsDataSourceModel struct {
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Backups types.List `tfsdk:"backups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerBackupsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_backups"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerBackupsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to list backups for a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID to list backups for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"backups": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The list of backups.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The backup ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The backup type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"created_at": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The backup creation timestamp.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerBackupsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerBackupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerBackupsDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/backups/server/%d?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server Backups", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var backupsResp client.BackupResponse
|
||||||
|
if err := json.Unmarshal(result, &backupsResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Backups Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backupAttrTypes := map[string]attr.Type{
|
||||||
|
"id": types.Int64Type,
|
||||||
|
"type": types.StringType,
|
||||||
|
"created_at": types.StringType,
|
||||||
|
}
|
||||||
|
|
||||||
|
backupObjects := make([]attr.Value, len(backupsResp.Data))
|
||||||
|
for i, b := range backupsResp.Data {
|
||||||
|
obj, diags := types.ObjectValue(
|
||||||
|
backupAttrTypes,
|
||||||
|
map[string]attr.Value{
|
||||||
|
"id": types.Int64Value(b.ID),
|
||||||
|
"type": types.StringValue(b.Type),
|
||||||
|
"created_at": types.StringValue(b.CreatedAt),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backupObjects[i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
backupsList, diags := types.ListValue(types.ObjectType{AttrTypes: backupAttrTypes}, backupObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Backups = backupsList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
166
internal/provider/data_source_server_firewall.go
Normal file
166
internal/provider/data_source_server_firewall.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerFirewallDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerFirewallDataSource{}
|
||||||
|
|
||||||
|
// NewServerFirewallDataSource returns a new data source for reading server firewall information.
|
||||||
|
func NewServerFirewallDataSource() datasource.DataSource {
|
||||||
|
return &ServerFirewallDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerFirewallDataSource defines the data source implementation.
|
||||||
|
type ServerFirewallDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerFirewallDataSourceModel describes the data source data model.
|
||||||
|
type ServerFirewallDataSourceModel struct {
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
InterfaceName types.String `tfsdk:"interface_name"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
Rules types.List `tfsdk:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerFirewallDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_firewall"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerFirewallDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to read firewall information for a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID to read firewall information for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"interface_name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The network interface name. Defaults to `eth0`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the firewall is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"rules": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The firewall rules.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"action": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The action for the rule (e.g. `accept`, `drop`).",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"direction": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The direction for the rule (e.g. `in`, `out`).",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"protocol": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The protocol for the rule (e.g. `tcp`, `udp`).",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"port": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The port or port range for the rule.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"ip": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IP address or CIDR for the rule.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerFirewallDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerFirewallDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerFirewallDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default interface_name to "eth0" if not set.
|
||||||
|
interfaceName := "eth0"
|
||||||
|
if !data.InterfaceName.IsNull() && !data.InterfaceName.IsUnknown() && data.InterfaceName.ValueString() != "" {
|
||||||
|
interfaceName = data.InterfaceName.ValueString()
|
||||||
|
}
|
||||||
|
data.InterfaceName = types.StringValue(interfaceName)
|
||||||
|
|
||||||
|
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", data.ServerID.ValueInt64(), interfaceName))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server Firewall", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fwResp client.FirewallResponse
|
||||||
|
if err := json.Unmarshal(result, &fwResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Firewall Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Enabled = types.BoolValue(fwResp.Data.Enabled)
|
||||||
|
|
||||||
|
ruleObjects := make([]attr.Value, len(fwResp.Data.Rules))
|
||||||
|
for i, rule := range fwResp.Data.Rules {
|
||||||
|
obj, diags := types.ObjectValue(
|
||||||
|
firewallRuleAttrTypes(),
|
||||||
|
map[string]attr.Value{
|
||||||
|
"action": types.StringValue(rule.Action),
|
||||||
|
"direction": types.StringValue(rule.Direction),
|
||||||
|
"protocol": types.StringValue(rule.Protocol),
|
||||||
|
"port": types.StringValue(rule.Port),
|
||||||
|
"ip": types.StringValue(rule.IP),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ruleObjects[i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesList, diags := types.ListValue(types.ObjectType{AttrTypes: firewallRuleAttrTypes()}, ruleObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Rules = rulesList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
138
internal/provider/data_source_server_templates.go
Normal file
138
internal/provider/data_source_server_templates.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerTemplatesDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerTemplatesDataSource{}
|
||||||
|
|
||||||
|
// NewServerTemplatesDataSource returns a new data source for listing server templates.
|
||||||
|
func NewServerTemplatesDataSource() datasource.DataSource {
|
||||||
|
return &ServerTemplatesDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTemplatesDataSource defines the data source implementation.
|
||||||
|
type ServerTemplatesDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTemplatesDataSourceModel describes the data source data model.
|
||||||
|
type ServerTemplatesDataSourceModel struct {
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Templates types.List `tfsdk:"templates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTemplatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTemplatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to list available templates for a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID to list templates for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"templates": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The list of available templates.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The template ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The template name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTemplatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTemplatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerTemplatesDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/%d/templates?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server Templates", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateResp client.TemplateResponse
|
||||||
|
if err := json.Unmarshal(result, &templateResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Templates Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateAttrTypes := map[string]attr.Type{
|
||||||
|
"id": types.Int64Type,
|
||||||
|
"name": types.StringType,
|
||||||
|
}
|
||||||
|
|
||||||
|
templateObjects := make([]attr.Value, len(templateResp.Data))
|
||||||
|
for i, t := range templateResp.Data {
|
||||||
|
obj, diags := types.ObjectValue(
|
||||||
|
templateAttrTypes,
|
||||||
|
map[string]attr.Value{
|
||||||
|
"id": types.Int64Value(t.ID),
|
||||||
|
"name": types.StringValue(t.Name),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateObjects[i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
templatesList, diags := types.ListValue(types.ObjectType{AttrTypes: templateAttrTypes}, templateObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Templates = templatesList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
102
internal/provider/data_source_server_traffic.go
Normal file
102
internal/provider/data_source_server_traffic.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerTrafficDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerTrafficDataSource{}
|
||||||
|
|
||||||
|
// NewServerTrafficDataSource returns a new data source for reading server traffic.
|
||||||
|
func NewServerTrafficDataSource() datasource.DataSource {
|
||||||
|
return &ServerTrafficDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTrafficDataSource defines the data source implementation.
|
||||||
|
type ServerTrafficDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTrafficDataSourceModel describes the data source data model.
|
||||||
|
type ServerTrafficDataSourceModel struct {
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Used types.Int64 `tfsdk:"used"`
|
||||||
|
Limit types.Int64 `tfsdk:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_traffic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to read traffic usage for a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID to read traffic for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"used": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The amount of traffic used.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"limit": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The traffic limit.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerTrafficDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/traffic", data.ServerID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server Traffic", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var trafficResp client.TrafficResponse
|
||||||
|
if err := json.Unmarshal(result, &trafficResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Traffic Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Used = types.Int64Value(trafficResp.Data.Used)
|
||||||
|
data.Limit = types.Int64Value(trafficResp.Data.Limit)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
138
internal/provider/data_source_server_traffic_blocks.go
Normal file
138
internal/provider/data_source_server_traffic_blocks.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerTrafficBlocksDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerTrafficBlocksDataSource{}
|
||||||
|
|
||||||
|
// NewServerTrafficBlocksDataSource returns a new data source for listing server traffic blocks.
|
||||||
|
func NewServerTrafficBlocksDataSource() datasource.DataSource {
|
||||||
|
return &ServerTrafficBlocksDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTrafficBlocksDataSource defines the data source implementation.
|
||||||
|
type ServerTrafficBlocksDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTrafficBlocksDataSourceModel describes the data source data model.
|
||||||
|
type ServerTrafficBlocksDataSourceModel struct {
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Blocks types.List `tfsdk:"blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficBlocksDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_traffic_blocks"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficBlocksDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to list traffic blocks for a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID to list traffic blocks for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"blocks": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The list of traffic blocks.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The traffic block ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The traffic block type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficBlocksDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerTrafficBlocksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerTrafficBlocksDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/%d/traffic/blocks?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server Traffic Blocks", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocksResp client.TrafficBlockListResponse
|
||||||
|
if err := json.Unmarshal(result, &blocksResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Traffic Blocks Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blockAttrTypes := map[string]attr.Type{
|
||||||
|
"id": types.Int64Type,
|
||||||
|
"type": types.StringType,
|
||||||
|
}
|
||||||
|
|
||||||
|
blockObjects := make([]attr.Value, len(blocksResp.Data))
|
||||||
|
for i, b := range blocksResp.Data {
|
||||||
|
obj, diags := types.ObjectValue(
|
||||||
|
blockAttrTypes,
|
||||||
|
map[string]attr.Value{
|
||||||
|
"id": types.Int64Value(b.ID),
|
||||||
|
"type": types.StringValue(b.Type),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blockObjects[i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
blocksList, diags := types.ListValue(types.ObjectType{AttrTypes: blockAttrTypes}, blockObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Blocks = blocksList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
97
internal/provider/data_source_server_vnc.go
Normal file
97
internal/provider/data_source_server_vnc.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServerVNCDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServerVNCDataSource{}
|
||||||
|
|
||||||
|
// NewServerVNCDataSource returns a new data source for reading server VNC information.
|
||||||
|
func NewServerVNCDataSource() datasource.DataSource {
|
||||||
|
return &ServerVNCDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerVNCDataSource defines the data source implementation.
|
||||||
|
type ServerVNCDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerVNCDataSourceModel describes the data source data model.
|
||||||
|
type ServerVNCDataSourceModel struct {
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
URL types.String `tfsdk:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerVNCDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_vnc"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerVNCDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to read VNC connection information for a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID to read VNC information for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"url": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The VNC connection URL.",
|
||||||
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerVNCDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServerVNCDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServerVNCDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/vnc", data.ServerID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server VNC", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var vncResp client.VNCResponse
|
||||||
|
if err := json.Unmarshal(result, &vncResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server VNC Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.URL = types.StringValue(vncResp.Data.URL)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
177
internal/provider/data_source_servers.go
Normal file
177
internal/provider/data_source_servers.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServersDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServersDataSource{}
|
||||||
|
|
||||||
|
// NewServersDataSource returns a new data source for listing all servers.
|
||||||
|
func NewServersDataSource() datasource.DataSource {
|
||||||
|
return &ServersDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServersDataSource defines the data source implementation.
|
||||||
|
type ServersDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServersDataSourceModel describes the data source data model.
|
||||||
|
type ServersDataSourceModel struct {
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Servers types.List `tfsdk:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_servers"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to list all VirtFusion servers.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"servers": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The list of servers.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: serverDataSourceSchemaAttributes(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServersDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers?%s", resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Servers", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.ServerListResponse
|
||||||
|
if err := json.Unmarshal(result, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Servers Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverObjects := make([]attr.Value, len(listResp.Data))
|
||||||
|
for i, s := range listResp.Data {
|
||||||
|
obj, diags := types.ObjectValue(
|
||||||
|
serverDataSourceAttrTypes(),
|
||||||
|
serverDataToAttrValues(s),
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverObjects[i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
serversList, diags := types.ListValue(types.ObjectType{AttrTypes: serverDataSourceAttrTypes()}, serverObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Servers = serversList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverDataSourceSchemaAttributes returns the schema attributes for a server object
|
||||||
|
// used in list data sources.
|
||||||
|
func serverDataSourceSchemaAttributes() map[string]schema.Attribute {
|
||||||
|
return map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"uuid": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server UUID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server display name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"hostname": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server hostname.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"owner_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The owner (user) ID who owns the server.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"hypervisor_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor ID where the server is hosted.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"suspended": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the server is suspended.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverDataSourceAttrTypes returns the attribute types for a server object.
|
||||||
|
func serverDataSourceAttrTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"id": types.Int64Type,
|
||||||
|
"uuid": types.StringType,
|
||||||
|
"name": types.StringType,
|
||||||
|
"hostname": types.StringType,
|
||||||
|
"owner_id": types.Int64Type,
|
||||||
|
"hypervisor_id": types.Int64Type,
|
||||||
|
"suspended": types.BoolType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverDataToAttrValues converts a client.ServerData to a map of attribute values.
|
||||||
|
func serverDataToAttrValues(s client.ServerData) map[string]attr.Value {
|
||||||
|
return map[string]attr.Value{
|
||||||
|
"id": types.Int64Value(s.ID),
|
||||||
|
"uuid": types.StringValue(s.UUID),
|
||||||
|
"name": types.StringValue(s.Name),
|
||||||
|
"hostname": types.StringValue(s.Hostname),
|
||||||
|
"owner_id": types.Int64Value(s.OwnerID),
|
||||||
|
"hypervisor_id": types.Int64Value(s.HypervisorID),
|
||||||
|
"suspended": types.BoolValue(s.Suspended),
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/provider/data_source_servers_by_user.go
Normal file
121
internal/provider/data_source_servers_by_user.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ datasource.DataSource = &ServersByUserDataSource{}
|
||||||
|
var _ datasource.DataSourceWithConfigure = &ServersByUserDataSource{}
|
||||||
|
|
||||||
|
// NewServersByUserDataSource returns a new data source for listing servers by user.
|
||||||
|
func NewServersByUserDataSource() datasource.DataSource {
|
||||||
|
return &ServersByUserDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServersByUserDataSource defines the data source implementation.
|
||||||
|
type ServersByUserDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServersByUserDataSourceModel describes the data source data model.
|
||||||
|
type ServersByUserDataSourceModel struct {
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
Servers types.List `tfsdk:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersByUserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_servers_by_user"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersByUserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Use this data source to list all VirtFusion servers owned by a specific user.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID to filter servers by.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"servers": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The list of servers owned by the user.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: serverDataSourceSchemaAttributes(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersByUserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ServersByUserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data ServersByUserDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/user/%d?%s", data.UserID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading Servers By User", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.ServerListResponse
|
||||||
|
if err := json.Unmarshal(result, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Servers Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverObjects := make([]attr.Value, len(listResp.Data))
|
||||||
|
for i, s := range listResp.Data {
|
||||||
|
obj, diags := types.ObjectValue(
|
||||||
|
serverDataSourceAttrTypes(),
|
||||||
|
serverDataToAttrValues(s),
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverObjects[i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
serversList, diags := types.ListValue(types.ObjectType{AttrTypes: serverDataSourceAttrTypes()}, serverObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Servers = serversList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
135
internal/provider/data_source_ssh_key.go
Normal file
135
internal/provider/data_source_ssh_key.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SSHKeyDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SSHKeyDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSSHKeyDataSource returns a new SSH key data source.
|
||||||
|
func NewSSHKeyDataSource() datasource.DataSource {
|
||||||
|
return &SSHKeyDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyDataSource defines the data source implementation.
|
||||||
|
type SSHKeyDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyDataSourceModel describes the data source data model.
|
||||||
|
type SSHKeyDataSourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
PublicKey types.String `tfsdk:"public_key"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
CreatedAt types.String `tfsdk:"created_at"`
|
||||||
|
UpdatedAt types.String `tfsdk:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeyDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_ssh_key"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a single VirtFusion SSH key by ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The SSH key ID.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The SSH key name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The SSH key type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"public_key": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The public key content.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the SSH key is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user who owns this SSH key.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"created_at": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The creation timestamp.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"updated_at": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The last update timestamp.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeyDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SSHKeyDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading SSH Key", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshKeyResp client.SSHKeyResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &sshKeyResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing SSH Key Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(sshKeyResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(sshKeyResp.Data.Name)
|
||||||
|
data.Type = types.StringValue(sshKeyResp.Data.Type)
|
||||||
|
data.PublicKey = types.StringValue(sshKeyResp.Data.PublicKey)
|
||||||
|
data.Enabled = types.BoolValue(sshKeyResp.Data.Enabled)
|
||||||
|
data.UserID = types.Int64Value(sshKeyResp.Data.UserID)
|
||||||
|
data.CreatedAt = types.StringValue(sshKeyResp.Data.CreatedAt)
|
||||||
|
data.UpdatedAt = types.StringValue(sshKeyResp.Data.UpdatedAt)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
142
internal/provider/data_source_ssh_keys_by_user.go
Normal file
142
internal/provider/data_source_ssh_keys_by_user.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &SSHKeysByUserDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &SSHKeysByUserDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSSHKeysByUserDataSource returns a new SSH keys by user data source.
|
||||||
|
func NewSSHKeysByUserDataSource() datasource.DataSource {
|
||||||
|
return &SSHKeysByUserDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeysByUserDataSource defines the data source implementation.
|
||||||
|
type SSHKeysByUserDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeysByUserDataSourceModel describes the data source data model.
|
||||||
|
type SSHKeysByUserDataSourceModel struct {
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
Results types.Int64 `tfsdk:"results"`
|
||||||
|
SSHKeys []SSHKeyByUserItemModel `tfsdk:"ssh_keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyByUserItemModel describes a single SSH key in the list.
|
||||||
|
type SSHKeyByUserItemModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
PublicKey types.String `tfsdk:"public_key"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeysByUserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_ssh_keys_by_user"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeysByUserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches all SSH keys for a VirtFusion user.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID to fetch SSH keys for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": resultsSchemaAttribute(),
|
||||||
|
"ssh_keys": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "List of SSH keys belonging to the user.",
|
||||||
|
Computed: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The SSH key ID.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The SSH key name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The SSH key type.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"public_key": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The public key content.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the SSH key is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeysByUserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SSHKeysByUserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data SSHKeysByUserDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/ssh_keys/user/%d?%s", data.UserID.ValueInt64(), resultsQueryParam(data.Results)))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading SSH Keys By User", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp client.SSHKeyListResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &listResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing SSH Keys Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.SSHKeys = make([]SSHKeyByUserItemModel, len(listResp.Data))
|
||||||
|
for i, k := range listResp.Data {
|
||||||
|
data.SSHKeys[i] = SSHKeyByUserItemModel{
|
||||||
|
ID: types.Int64Value(k.ID),
|
||||||
|
Name: types.StringValue(k.Name),
|
||||||
|
Type: types.StringValue(k.Type),
|
||||||
|
PublicKey: types.StringValue(k.PublicKey),
|
||||||
|
Enabled: types.BoolValue(k.Enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
128
internal/provider/data_source_user.go
Normal file
128
internal/provider/data_source_user.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.DataSource = &UserDataSource{}
|
||||||
|
_ datasource.DataSourceWithConfigure = &UserDataSource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUserDataSource returns a new user data source.
|
||||||
|
func NewUserDataSource() datasource.DataSource {
|
||||||
|
return &UserDataSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDataSource defines the data source implementation.
|
||||||
|
type UserDataSource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDataSourceModel describes the data source data model.
|
||||||
|
type UserDataSourceModel struct {
|
||||||
|
ExtRelationID types.String `tfsdk:"ext_relation_id"`
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Email types.String `tfsdk:"email"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
CreatedAt types.String `tfsdk:"created_at"`
|
||||||
|
UpdatedAt types.String `tfsdk:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_user"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Fetches a VirtFusion user by external relation ID.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"ext_relation_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The external relation ID of the user.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The numeric ID of the user.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The user name.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"email": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The user email address.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the user is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"created_at": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The creation timestamp.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"updated_at": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The last update timestamp.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Data Source Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
|
var data UserDataSourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Reading User", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userResp client.UserResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &userResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing User Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(userResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(userResp.Data.Name)
|
||||||
|
data.Email = types.StringValue(userResp.Data.Email)
|
||||||
|
data.Enabled = types.BoolValue(userResp.Data.Enabled)
|
||||||
|
data.CreatedAt = types.StringValue(userResp.Data.CreatedAt)
|
||||||
|
data.UpdatedAt = types.StringValue(userResp.Data.UpdatedAt)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
@@ -1,141 +1,181 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) EZSCALE.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure VirtfusionProvider satisfies various provider interfaces.
|
const defaultResultsPerPage int64 = 300
|
||||||
|
|
||||||
|
// resultsSchemaAttribute returns the standard "results" schema attribute for list data sources.
|
||||||
|
func resultsSchemaAttribute() dsschema.Int64Attribute {
|
||||||
|
return dsschema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Maximum number of results to return. Defaults to 300.",
|
||||||
|
Optional: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resultsQueryParam returns the query parameter string for the results limit.
|
||||||
|
// If results is null/unknown, defaults to defaultResultsPerPage.
|
||||||
|
func resultsQueryParam(results types.Int64) string {
|
||||||
|
n := defaultResultsPerPage
|
||||||
|
if !results.IsNull() && !results.IsUnknown() {
|
||||||
|
n = results.ValueInt64()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("results=%d", n)
|
||||||
|
}
|
||||||
|
|
||||||
var _ provider.Provider = &VirtfusionProvider{}
|
var _ provider.Provider = &VirtfusionProvider{}
|
||||||
|
|
||||||
// VirtfusionProvider defines the provider implementation.
|
// VirtfusionProvider defines the provider implementation.
|
||||||
type VirtfusionProvider struct {
|
type VirtfusionProvider 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
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScaffoldingProviderModel describes the provider data model.
|
// VirtfusionProviderModel describes the provider data model.
|
||||||
type ScaffoldingProviderModel struct {
|
type VirtfusionProviderModel struct {
|
||||||
Endpoint types.String `tfsdk:"endpoint"`
|
Endpoint types.String `tfsdk:"endpoint"`
|
||||||
ApiToken types.String `tfsdk:"api_token"`
|
ApiToken types.String `tfsdk:"api_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *VirtfusionProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
|
func (p *VirtfusionProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||||
resp.TypeName = "virtfusion"
|
resp.TypeName = "virtfusion"
|
||||||
resp.Version = p.version
|
resp.Version = p.version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *VirtfusionProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
|
func (p *VirtfusionProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "The VirtFusion provider allows managing VirtFusion virtualization platform resources.",
|
||||||
Attributes: map[string]schema.Attribute{
|
Attributes: map[string]schema.Attribute{
|
||||||
"endpoint": schema.StringAttribute{
|
"endpoint": schema.StringAttribute{
|
||||||
MarkdownDescription: "The endpoint to use for API requests.",
|
MarkdownDescription: "The VirtFusion API endpoint. Can be a hostname (e.g. `cp.example.com`) or full URL (e.g. `https://cp.example.com/api/v1`). Can also be set with the `VIRTFUSION_ENDPOINT` environment variable.",
|
||||||
Required: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
"api_token": schema.StringAttribute{
|
"api_token": schema.StringAttribute{
|
||||||
MarkdownDescription: "The API token to use for API requests.",
|
MarkdownDescription: "The API token for authentication. Can also be set with the `VIRTFUSION_API_TOKEN` environment variable.",
|
||||||
Required: true,
|
Optional: true,
|
||||||
|
Sensitive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *VirtfusionProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
func (p *VirtfusionProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||||
// Check environment variables
|
var data VirtfusionProviderModel
|
||||||
apiToken := os.Getenv("VIRTFUSION_API_TOKEN")
|
|
||||||
endpoint := os.Getenv("VIRTFUSION_ENDPOINT")
|
|
||||||
|
|
||||||
var data ScaffoldingProviderModel
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
if resp.Diagnostics.HasError() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration values are now available.
|
// Environment variables as fallback
|
||||||
// if data.Endpoint.IsNull() { /* ... */ }
|
endpoint := os.Getenv("VIRTFUSION_ENDPOINT")
|
||||||
|
apiToken := os.Getenv("VIRTFUSION_API_TOKEN")
|
||||||
|
|
||||||
if data.Endpoint.ValueString() != "" {
|
// Config values override env vars
|
||||||
|
if !data.Endpoint.IsNull() && data.Endpoint.ValueString() != "" {
|
||||||
endpoint = data.Endpoint.ValueString()
|
endpoint = data.Endpoint.ValueString()
|
||||||
}
|
}
|
||||||
|
if !data.ApiToken.IsNull() && data.ApiToken.ValueString() != "" {
|
||||||
if data.ApiToken.ValueString() != "" {
|
|
||||||
apiToken = data.ApiToken.ValueString()
|
apiToken = data.ApiToken.ValueString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiToken == "" {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Missing API Token Configuration",
|
|
||||||
"While configuring the provider, the API token was not found in "+
|
|
||||||
"the VIRTFUSION_API_TOKEN environment variable or provider "+
|
|
||||||
"configuration block api_token attribute.",
|
|
||||||
)
|
|
||||||
// Not returning early allows the logic to collect all errors.
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
resp.Diagnostics.AddError(
|
resp.Diagnostics.AddError(
|
||||||
"Missing Endpoint Configuration",
|
"Missing Endpoint Configuration",
|
||||||
"While configuring the provider, the endpoint was not found in "+
|
"The VirtFusion endpoint was not found in the VIRTFUSION_ENDPOINT environment variable or provider configuration block endpoint attribute.",
|
||||||
"the VIRTFUSION_ENDPOINT environment variable or provider "+
|
|
||||||
"configuration block endpoint attribute.",
|
|
||||||
)
|
)
|
||||||
// Not returning early allows the logic to collect all errors.
|
}
|
||||||
|
if apiToken == "" {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Missing API Token Configuration",
|
||||||
|
"The API token was not found in the VIRTFUSION_API_TOKEN environment variable or provider configuration block api_token attribute.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
customTransport := &CustomTransport{
|
c, err := client.New(endpoint, apiToken)
|
||||||
Transport: http.DefaultTransport,
|
if err != nil {
|
||||||
BaseURL: &url.URL{Scheme: "https", Host: endpoint, Path: "/api/v1"},
|
resp.Diagnostics.AddError("Failed to Create Client", err.Error())
|
||||||
Token: apiToken,
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example client configuration for data sources and resources
|
resp.DataSourceData = c
|
||||||
client := &http.Client{
|
resp.ResourceData = c
|
||||||
Transport: customTransport,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.DataSourceData = client
|
|
||||||
resp.ResourceData = client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *VirtfusionProvider) Resources(ctx context.Context) []func() resource.Resource {
|
func (p *VirtfusionProvider) Resources(_ context.Context) []func() resource.Resource {
|
||||||
return []func() resource.Resource{
|
return []func() resource.Resource{
|
||||||
NewVirtfusionServerResource,
|
NewServerResource,
|
||||||
NewVirtfusionServerBuildResource,
|
NewServerBuildResource,
|
||||||
NewVirtfusionSSHResource,
|
NewSSHKeyResource,
|
||||||
|
NewUserResource,
|
||||||
|
NewServerFirewallResource,
|
||||||
|
NewServerNetworkWhitelistResource,
|
||||||
|
NewServerIPv4Resource,
|
||||||
|
NewServerTrafficBlockResource,
|
||||||
|
NewIPBlockRangeResource,
|
||||||
|
NewSelfServiceCreditResource,
|
||||||
|
NewSelfServiceResourcePackResource,
|
||||||
|
NewSelfServiceHourlyGroupProfileResource,
|
||||||
|
NewSelfServiceResourceGroupProfileResource,
|
||||||
|
NewServerPowerActionResource,
|
||||||
|
NewServerPasswordResetResource,
|
||||||
|
NewUserPasswordResetResource,
|
||||||
|
NewUserAuthTokenResource,
|
||||||
|
NewUserServerAuthTokenResource,
|
||||||
|
NewSelfServicePackServersActionResource,
|
||||||
|
NewSelfServiceHourlyResourcePackResource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *VirtfusionProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
|
func (p *VirtfusionProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||||||
return []func() datasource.DataSource{}
|
return []func() datasource.DataSource{
|
||||||
}
|
NewHypervisorDataSource,
|
||||||
|
NewHypervisorsDataSource,
|
||||||
type CustomTransport struct {
|
NewHypervisorGroupDataSource,
|
||||||
Transport http.RoundTripper
|
NewHypervisorGroupsDataSource,
|
||||||
BaseURL *url.URL
|
NewHypervisorGroupResourcesDataSource,
|
||||||
Token string
|
NewServerDataSource,
|
||||||
}
|
NewServersDataSource,
|
||||||
|
NewServersByUserDataSource,
|
||||||
func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
NewServerTemplatesDataSource,
|
||||||
req.Header.Add("Authorization", "Bearer "+c.Token)
|
NewServerTrafficDataSource,
|
||||||
req.URL.Scheme = c.BaseURL.Scheme
|
NewServerTrafficBlocksDataSource,
|
||||||
req.URL.Host = c.BaseURL.Host
|
NewServerVNCDataSource,
|
||||||
req.URL.Path = path.Join(c.BaseURL.Path, req.URL.Path)
|
NewServerBackupsDataSource,
|
||||||
return c.Transport.RoundTrip(req)
|
NewServerFirewallDataSource,
|
||||||
|
NewPackageDataSource,
|
||||||
|
NewPackagesDataSource,
|
||||||
|
NewPackageTemplatesDataSource,
|
||||||
|
NewIPBlockDataSource,
|
||||||
|
NewIPBlocksDataSource,
|
||||||
|
NewSSHKeyDataSource,
|
||||||
|
NewSSHKeysByUserDataSource,
|
||||||
|
NewUserDataSource,
|
||||||
|
NewDNSServiceDataSource,
|
||||||
|
NewISODataSource,
|
||||||
|
NewQueueItemDataSource,
|
||||||
|
NewSelfServiceCurrenciesDataSource,
|
||||||
|
NewSelfServiceResourcePackDataSource,
|
||||||
|
NewSelfServiceHourlyStatsDataSource,
|
||||||
|
NewSelfServiceReportDataSource,
|
||||||
|
NewSelfServiceUsageDataSource,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(version string) func() provider.Provider {
|
func New(version string) func() provider.Provider {
|
||||||
|
|||||||
162
internal/provider/resource_ip_block_range.go
Normal file
162
internal/provider/resource_ip_block_range.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &IPBlockRangeResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &IPBlockRangeResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewIPBlockRangeResource creates a new IP block range resource.
|
||||||
|
func NewIPBlockRangeResource() resource.Resource {
|
||||||
|
return &IPBlockRangeResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockRangeResource defines the resource implementation.
|
||||||
|
type IPBlockRangeResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPBlockRangeResourceModel describes the resource data model.
|
||||||
|
type IPBlockRangeResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
IPBlockID types.Int64 `tfsdk:"ip_block_id"`
|
||||||
|
StartIP types.String `tfsdk:"start_ip"`
|
||||||
|
EndIP types.String `tfsdk:"end_ip"`
|
||||||
|
Gateway types.String `tfsdk:"gateway"`
|
||||||
|
Netmask types.String `tfsdk:"netmask"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_ip_block_range"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Adds an IPv4 address range to a VirtFusion IP block. This is a create-only resource — ranges cannot be deleted via the API.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier of the IP block range.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ip_block_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the IP block to add the range to.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"start_ip": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The starting IP address of the range.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"end_ip": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The ending IP address of the range.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gateway": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The gateway address for the range.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"netmask": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The netmask for the range.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data IPBlockRangeResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeReq := client.IPBlockRangeRequest{
|
||||||
|
StartIP: data.StartIP.ValueString(),
|
||||||
|
EndIP: data.EndIP.ValueString(),
|
||||||
|
Gateway: data.Gateway.ValueString(),
|
||||||
|
Netmask: data.Netmask.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/connectivity/ipblocks/%d/ipv4", data.IPBlockID.ValueInt64())
|
||||||
|
_, err := r.client.Post(ctx, apiPath, rangeReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating IP Block Range",
|
||||||
|
fmt.Sprintf("Could not create IP block range on block %d: %s", data.IPBlockID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a composite ID since the API does not return one.
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/%s-%s", data.IPBlockID.ValueInt64(), data.StartIP.ValueString(), data.EndIP.ValueString()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data IPBlockRangeResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
|
||||||
|
// All attributes require replacement — updates are never called.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IPBlockRangeResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No delete API — removing from state only.
|
||||||
|
}
|
||||||
173
internal/provider/resource_self_service_credit.go
Normal file
173
internal/provider/resource_self_service_credit.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SelfServiceCreditResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SelfServiceCreditResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceCreditResource creates a new self-service credit resource.
|
||||||
|
func NewSelfServiceCreditResource() resource.Resource {
|
||||||
|
return &SelfServiceCreditResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCreditResource defines the resource implementation.
|
||||||
|
type SelfServiceCreditResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceCreditResourceModel describes the resource data model.
|
||||||
|
type SelfServiceCreditResourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Amount types.Float64 `tfsdk:"amount"`
|
||||||
|
CurrencyCode types.String `tfsdk:"currency_code"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_credit"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages self-service credit in VirtFusion. Deleting this resource cancels the credit.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The identifier of the credit entry.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"amount": schema.Float64Attribute{
|
||||||
|
MarkdownDescription: "The credit amount.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Float64{
|
||||||
|
float64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"currency_code": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The currency code (e.g. `USD`, `EUR`).",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user to add credit to.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SelfServiceCreditResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creditReq := client.SelfServiceCreditRequest{
|
||||||
|
Amount: data.Amount.ValueFloat64(),
|
||||||
|
CurrencyCode: data.CurrencyCode.ValueString(),
|
||||||
|
UserID: data.UserID.ValueInt64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := r.client.Post(ctx, "/selfService/credit", creditReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating Credit",
|
||||||
|
fmt.Sprintf("Could not create credit for user %d: %s", data.UserID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditResp client.SelfServiceCreditResponse
|
||||||
|
if err := json.Unmarshal(respBody, &creditResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Parsing Response",
|
||||||
|
fmt.Sprintf("Could not parse credit response: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(creditResp.Data.ID)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SelfServiceCreditResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
|
||||||
|
// All attributes require replacement — updates are never called.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceCreditResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data SelfServiceCreditResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/credit/%d", data.ID.ValueInt64())
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Cancelling Credit",
|
||||||
|
fmt.Sprintf("Could not cancel credit %d: %s", data.ID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
172
internal/provider/resource_self_service_hourly_group_profile.go
Normal file
172
internal/provider/resource_self_service_hourly_group_profile.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SelfServiceHourlyGroupProfileResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SelfServiceHourlyGroupProfileResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceHourlyGroupProfileResource creates a new self-service hourly group profile resource.
|
||||||
|
func NewSelfServiceHourlyGroupProfileResource() resource.Resource {
|
||||||
|
return &SelfServiceHourlyGroupProfileResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyGroupProfileResource defines the resource implementation.
|
||||||
|
type SelfServiceHourlyGroupProfileResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyGroupProfileResourceModel describes the resource data model.
|
||||||
|
type SelfServiceHourlyGroupProfileResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
GroupID types.Int64 `tfsdk:"group_id"`
|
||||||
|
ProfileID types.Int64 `tfsdk:"profile_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_hourly_group_profile"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a self-service hourly group profile assignment in VirtFusion.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The composite identifier of the hourly group profile (userId/groupId/profileId).",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"group_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the hypervisor group.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"profile_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the hourly profile.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SelfServiceHourlyGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profileReq := map[string]int64{
|
||||||
|
"userId": data.UserID.ValueInt64(),
|
||||||
|
"groupId": data.GroupID.ValueInt64(),
|
||||||
|
"profileId": data.ProfileID.ValueInt64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.Post(ctx, "/selfService/hourlyGroupProfile", profileReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating Hourly Group Profile",
|
||||||
|
fmt.Sprintf("Could not create hourly group profile for user %d: %s", data.UserID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a composite ID.
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/%d/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SelfServiceHourlyGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
|
||||||
|
// All attributes require replacement — updates are never called.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyGroupProfileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data SelfServiceHourlyGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/hourlyGroupProfile/%d/%d/%d",
|
||||||
|
data.UserID.ValueInt64(),
|
||||||
|
data.GroupID.ValueInt64(),
|
||||||
|
data.ProfileID.ValueInt64(),
|
||||||
|
)
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Deleting Hourly Group Profile",
|
||||||
|
fmt.Sprintf("Could not delete hourly group profile for user %d, group %d, profile %d: %s",
|
||||||
|
data.UserID.ValueInt64(),
|
||||||
|
data.GroupID.ValueInt64(),
|
||||||
|
data.ProfileID.ValueInt64(),
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
186
internal/provider/resource_self_service_hourly_resource_pack.go
Normal file
186
internal/provider/resource_self_service_hourly_resource_pack.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SelfServiceHourlyResourcePackResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SelfServiceHourlyResourcePackResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceHourlyResourcePackResource creates a new self-service hourly resource pack resource.
|
||||||
|
func NewSelfServiceHourlyResourcePackResource() resource.Resource {
|
||||||
|
return &SelfServiceHourlyResourcePackResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyResourcePackResource defines the resource implementation.
|
||||||
|
type SelfServiceHourlyResourcePackResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceHourlyResourcePackResourceModel describes the resource data model.
|
||||||
|
type SelfServiceHourlyResourcePackResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
GroupID types.Int64 `tfsdk:"group_id"`
|
||||||
|
ResourcePackID types.Int64 `tfsdk:"resource_pack_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_hourly_resource_pack"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Sets the hourly resource pack for a user and group in VirtFusion self-service. Changing any attribute forces recreation.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The composite identifier for this hourly resource pack assignment.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"group_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the group.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"resource_pack_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the resource pack.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SelfServiceHourlyResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/hourlyResourcePack/byUser/%d/group/%d/resourcePack/%d",
|
||||||
|
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64())
|
||||||
|
_, err := r.client.Put(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Setting Hourly Resource Pack",
|
||||||
|
fmt.Sprintf("Could not set hourly resource pack (user=%d, group=%d, resource_pack=%d): %s",
|
||||||
|
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d-%d-%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SelfServiceHourlyResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is. The API does not provide a direct read endpoint for this assignment.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
// All attributes have RequiresReplace, so Update should never be called.
|
||||||
|
var data SelfServiceHourlyResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: removing the hourly resource pack assignment from state only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *SelfServiceHourlyResourcePackResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data SelfServiceHourlyResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate user_id is positive.
|
||||||
|
if !data.UserID.IsNull() && !data.UserID.IsUnknown() && data.UserID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("user_id"),
|
||||||
|
"Invalid User ID",
|
||||||
|
"user_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate group_id is positive.
|
||||||
|
if !data.GroupID.IsNull() && !data.GroupID.IsUnknown() && data.GroupID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("group_id"),
|
||||||
|
"Invalid Group ID",
|
||||||
|
"group_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate resource_pack_id is positive.
|
||||||
|
if !data.ResourcePackID.IsNull() && !data.ResourcePackID.IsUnknown() && data.ResourcePackID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("resource_pack_id"),
|
||||||
|
"Invalid Resource Pack ID",
|
||||||
|
"resource_pack_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
204
internal/provider/resource_self_service_pack_servers_action.go
Normal file
204
internal/provider/resource_self_service_pack_servers_action.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/mapplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SelfServicePackServersActionResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SelfServicePackServersActionResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServicePackServersActionResource creates a new self-service pack servers action resource.
|
||||||
|
func NewSelfServicePackServersActionResource() resource.Resource {
|
||||||
|
return &SelfServicePackServersActionResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServicePackServersActionResource defines the resource implementation.
|
||||||
|
type SelfServicePackServersActionResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServicePackServersActionResourceModel describes the resource data model.
|
||||||
|
type SelfServicePackServersActionResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
PackID types.Int64 `tfsdk:"pack_id"`
|
||||||
|
Action types.String `tfsdk:"action"`
|
||||||
|
Triggers types.Map `tfsdk:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_pack_servers_action"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Performs an action on all servers in a self-service resource pack. This is a trigger-style resource — the action is executed on create and can be re-triggered by changing the `triggers` attribute.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier for this pack servers action.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"pack_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the resource pack.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"action": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The action to perform on the pack servers. Must be one of: `suspend`, `unsuspend`, `delete`.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"triggers": schema.MapAttribute{
|
||||||
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the action to be re-executed. Works like `triggers` in `terraform_data`.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.Map{
|
||||||
|
mapplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SelfServicePackServersActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := data.Action.ValueString()
|
||||||
|
packID := data.PackID.ValueInt64()
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "suspend":
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers/suspend", packID)
|
||||||
|
_, err := r.client.Post(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Suspending Pack Servers",
|
||||||
|
fmt.Sprintf("Could not suspend servers for resource pack %d: %s", packID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "unsuspend":
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers/unsuspend", packID)
|
||||||
|
_, err := r.client.Post(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Unsuspending Pack Servers",
|
||||||
|
fmt.Sprintf("Could not unsuspend servers for resource pack %d: %s", packID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "delete":
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers", packID)
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Deleting Pack Servers",
|
||||||
|
fmt.Sprintf("Could not delete servers for resource pack %d: %s", packID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d-%s-%d", packID, action, time.Now().UnixNano()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SelfServicePackServersActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is for trigger-style resources.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data SelfServicePackServersActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServicePackServersActionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: pack server actions are not reversible. Removing from state only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *SelfServicePackServersActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data SelfServicePackServersActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pack_id is positive.
|
||||||
|
if !data.PackID.IsNull() && !data.PackID.IsUnknown() && data.PackID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("pack_id"),
|
||||||
|
"Invalid Pack ID",
|
||||||
|
"pack_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate action is one of the allowed values.
|
||||||
|
if !data.Action.IsNull() && !data.Action.IsUnknown() {
|
||||||
|
action := data.Action.ValueString()
|
||||||
|
validActions := map[string]bool{
|
||||||
|
"suspend": true,
|
||||||
|
"unsuspend": true,
|
||||||
|
"delete": true,
|
||||||
|
}
|
||||||
|
if !validActions[action] {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("action"),
|
||||||
|
"Invalid Pack Servers Action",
|
||||||
|
fmt.Sprintf("action must be one of: suspend, unsuspend, delete. Got: %q", action),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SelfServiceResourceGroupProfileResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SelfServiceResourceGroupProfileResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceResourceGroupProfileResource creates a new self-service resource group profile resource.
|
||||||
|
func NewSelfServiceResourceGroupProfileResource() resource.Resource {
|
||||||
|
return &SelfServiceResourceGroupProfileResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourceGroupProfileResource defines the resource implementation.
|
||||||
|
type SelfServiceResourceGroupProfileResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourceGroupProfileResourceModel describes the resource data model.
|
||||||
|
type SelfServiceResourceGroupProfileResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
GroupID types.Int64 `tfsdk:"group_id"`
|
||||||
|
ProfileID types.Int64 `tfsdk:"profile_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_resource_group_profile"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Associates a resource group profile with a user in VirtFusion self-service. Changing any attribute forces recreation of the association.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The composite identifier for this resource group profile association.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"group_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the resource group.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"profile_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the profile.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SelfServiceResourceGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]int64{
|
||||||
|
"userId": data.UserID.ValueInt64(),
|
||||||
|
"groupId": data.GroupID.ValueInt64(),
|
||||||
|
"profileId": data.ProfileID.ValueInt64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.Post(ctx, "/selfService/resourceGroupProfile", body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating Resource Group Profile Association",
|
||||||
|
fmt.Sprintf("Could not create resource group profile association (user=%d, group=%d, profile=%d): %s",
|
||||||
|
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d-%d-%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SelfServiceResourceGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is. The API does not provide a direct read endpoint for this association.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
// All attributes have RequiresReplace, so Update should never be called.
|
||||||
|
var data SelfServiceResourceGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data SelfServiceResourceGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourceGroupProfile/%d/%d/%d",
|
||||||
|
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64())
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Deleting Resource Group Profile Association",
|
||||||
|
fmt.Sprintf("Could not delete resource group profile association (user=%d, group=%d, profile=%d): %s",
|
||||||
|
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *SelfServiceResourceGroupProfileResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data SelfServiceResourceGroupProfileResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate user_id is positive.
|
||||||
|
if !data.UserID.IsNull() && !data.UserID.IsUnknown() && data.UserID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("user_id"),
|
||||||
|
"Invalid User ID",
|
||||||
|
"user_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate group_id is positive.
|
||||||
|
if !data.GroupID.IsNull() && !data.GroupID.IsUnknown() && data.GroupID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("group_id"),
|
||||||
|
"Invalid Group ID",
|
||||||
|
"group_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate profile_id is positive.
|
||||||
|
if !data.ProfileID.IsNull() && !data.ProfileID.IsUnknown() && data.ProfileID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("profile_id"),
|
||||||
|
"Invalid Profile ID",
|
||||||
|
"profile_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
215
internal/provider/resource_self_service_resource_pack.go
Normal file
215
internal/provider/resource_self_service_resource_pack.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SelfServiceResourcePackResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SelfServiceResourcePackResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSelfServiceResourcePackResource creates a new self-service resource pack resource.
|
||||||
|
func NewSelfServiceResourcePackResource() resource.Resource {
|
||||||
|
return &SelfServiceResourcePackResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackResource defines the resource implementation.
|
||||||
|
type SelfServiceResourcePackResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfServiceResourcePackResourceModel describes the resource data model.
|
||||||
|
type SelfServiceResourcePackResourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
PackID types.Int64 `tfsdk:"pack_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_self_service_resource_pack"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a self-service resource pack in VirtFusion.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The identifier of the resource pack.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The name of the resource pack.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user who owns the resource pack.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"pack_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the pack.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SelfServiceResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packReq := client.SelfServiceResourcePackRequest{
|
||||||
|
Name: data.Name.ValueString(),
|
||||||
|
UserID: data.UserID.ValueInt64(),
|
||||||
|
PackID: data.PackID.ValueInt64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := r.client.Post(ctx, "/selfService/resourcePack", packReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating Resource Pack",
|
||||||
|
fmt.Sprintf("Could not create resource pack: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var packResp client.SelfServiceResourcePackResponse
|
||||||
|
if err := json.Unmarshal(respBody, &packResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Parsing Response",
|
||||||
|
fmt.Sprintf("Could not parse resource pack response: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(packResp.Data.ID)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SelfServiceResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
|
||||||
|
respBody, err := r.client.Get(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
// Resource no longer exists, remove from state.
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Reading Resource Pack",
|
||||||
|
fmt.Sprintf("Could not read resource pack %d: %s", data.ID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var packResp client.SelfServiceResourcePackResponse
|
||||||
|
if err := json.Unmarshal(respBody, &packResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Parsing Response",
|
||||||
|
fmt.Sprintf("Could not parse resource pack response: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Name = types.StringValue(packResp.Data.Name)
|
||||||
|
data.UserID = types.Int64Value(packResp.Data.UserID)
|
||||||
|
data.PackID = types.Int64Value(packResp.Data.PackID)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data SelfServiceResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packReq := client.SelfServiceResourcePackRequest{
|
||||||
|
Name: data.Name.ValueString(),
|
||||||
|
UserID: data.UserID.ValueInt64(),
|
||||||
|
PackID: data.PackID.ValueInt64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
|
||||||
|
_, err := r.client.Put(ctx, apiPath, packReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Updating Resource Pack",
|
||||||
|
fmt.Sprintf("Could not update resource pack %d: %s", data.ID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SelfServiceResourcePackResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data SelfServiceResourcePackResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Deleting Resource Pack",
|
||||||
|
fmt.Sprintf("Could not delete resource pack %d: %s", data.ID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
637
internal/provider/resource_server.go
Normal file
637
internal/provider/resource_server.go
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/booldefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerResource{}
|
||||||
|
_ resource.ResourceWithImportState = &ServerResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerResource returns a new server resource.
|
||||||
|
func NewServerResource() resource.Resource {
|
||||||
|
return &ServerResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerResource defines the resource implementation.
|
||||||
|
type ServerResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerResourceModel describes the resource data model.
|
||||||
|
type ServerResourceModel struct {
|
||||||
|
// Computed
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
UUID types.String `tfsdk:"uuid"`
|
||||||
|
Hostname types.String `tfsdk:"hostname"`
|
||||||
|
|
||||||
|
// Required (create)
|
||||||
|
PackageID types.Int64 `tfsdk:"package_id"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
HypervisorID types.Int64 `tfsdk:"hypervisor_id"`
|
||||||
|
|
||||||
|
// Optional (create)
|
||||||
|
Ipv4 types.Int64 `tfsdk:"ipv4"`
|
||||||
|
Storage types.Int64 `tfsdk:"storage"`
|
||||||
|
Memory types.Int64 `tfsdk:"memory"`
|
||||||
|
Cores types.Int64 `tfsdk:"cores"`
|
||||||
|
Traffic types.Int64 `tfsdk:"traffic"`
|
||||||
|
InboundNetworkSpeed types.Int64 `tfsdk:"inbound_network_speed"`
|
||||||
|
OutboundNetworkSpeed types.Int64 `tfsdk:"outbound_network_speed"`
|
||||||
|
StorageProfile types.Int64 `tfsdk:"storage_profile"`
|
||||||
|
NetworkProfile types.Int64 `tfsdk:"network_profile"`
|
||||||
|
DryRun types.Bool `tfsdk:"dry_run"`
|
||||||
|
AdditionalStorage1 types.Int64 `tfsdk:"additional_storage_1"`
|
||||||
|
AdditionalStorage1Profile types.Int64 `tfsdk:"additional_storage_1_profile"`
|
||||||
|
AdditionalStorage2 types.Int64 `tfsdk:"additional_storage_2"`
|
||||||
|
AdditionalStorage2Profile types.Int64 `tfsdk:"additional_storage_2_profile"`
|
||||||
|
|
||||||
|
// Optional (update-only)
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
CPUThrottle types.Int64 `tfsdk:"cpu_throttle"`
|
||||||
|
VNCEnabled types.Bool `tfsdk:"vnc_enabled"`
|
||||||
|
Suspended types.Bool `tfsdk:"suspended"`
|
||||||
|
BackupPlanID types.Int64 `tfsdk:"backup_plan_id"`
|
||||||
|
CustomXML types.String `tfsdk:"custom_xml"`
|
||||||
|
OwnerUserID types.Int64 `tfsdk:"owner_user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
// Computed
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The server ID.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"uuid": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server UUID.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hostname": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server hostname.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Required
|
||||||
|
"package_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The package ID for the server.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID who owns the server.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"hypervisor_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The hypervisor ID where the server will be created.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optional (create)
|
||||||
|
"ipv4": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Number of IPv4 addresses to assign. Defaults to 1.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: int64default.StaticInt64(1),
|
||||||
|
},
|
||||||
|
"storage": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Storage size override in GB.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"memory": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Memory size override in MB.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"cores": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Number of CPU cores override.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"traffic": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Traffic limit override in GB.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"inbound_network_speed": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Inbound network speed override in Mbps.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"outbound_network_speed": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Outbound network speed override in Mbps.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"storage_profile": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Storage profile ID.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"network_profile": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Network profile ID.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"dry_run": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "If true, validates the request without creating the server.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
},
|
||||||
|
"additional_storage_1": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Additional storage 1 size in GB.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"additional_storage_1_profile": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Additional storage 1 profile ID.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"additional_storage_2": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Additional storage 2 size in GB.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"additional_storage_2_profile": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Additional storage 2 profile ID.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optional (update-only)
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The server display name.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"cpu_throttle": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "CPU throttle percentage (0-100).",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"vnc_enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether VNC is enabled on the server.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"suspended": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the server is suspended.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"backup_plan_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Backup plan ID. Set to 0 to remove the backup plan.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"custom_xml": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Custom XML configuration for the server.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"owner_user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The user ID to transfer ownership to.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var plan ServerResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the create request from plan values.
|
||||||
|
createReq := client.ServerCreateRequest{
|
||||||
|
PackageID: plan.PackageID.ValueInt64(),
|
||||||
|
UserID: plan.UserID.ValueInt64(),
|
||||||
|
HypervisorID: plan.HypervisorID.ValueInt64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !plan.Ipv4.IsNull() && !plan.Ipv4.IsUnknown() {
|
||||||
|
v := plan.Ipv4.ValueInt64()
|
||||||
|
createReq.Ipv4 = &v
|
||||||
|
}
|
||||||
|
if !plan.Storage.IsNull() && !plan.Storage.IsUnknown() {
|
||||||
|
v := plan.Storage.ValueInt64()
|
||||||
|
createReq.Storage = &v
|
||||||
|
}
|
||||||
|
if !plan.Memory.IsNull() && !plan.Memory.IsUnknown() {
|
||||||
|
v := plan.Memory.ValueInt64()
|
||||||
|
createReq.Memory = &v
|
||||||
|
}
|
||||||
|
if !plan.Cores.IsNull() && !plan.Cores.IsUnknown() {
|
||||||
|
v := plan.Cores.ValueInt64()
|
||||||
|
createReq.CPUCores = &v
|
||||||
|
}
|
||||||
|
if !plan.Traffic.IsNull() && !plan.Traffic.IsUnknown() {
|
||||||
|
v := plan.Traffic.ValueInt64()
|
||||||
|
createReq.Traffic = &v
|
||||||
|
}
|
||||||
|
if !plan.InboundNetworkSpeed.IsNull() && !plan.InboundNetworkSpeed.IsUnknown() {
|
||||||
|
v := plan.InboundNetworkSpeed.ValueInt64()
|
||||||
|
createReq.NetworkSpeedInbound = &v
|
||||||
|
}
|
||||||
|
if !plan.OutboundNetworkSpeed.IsNull() && !plan.OutboundNetworkSpeed.IsUnknown() {
|
||||||
|
v := plan.OutboundNetworkSpeed.ValueInt64()
|
||||||
|
createReq.NetworkSpeedOutbound = &v
|
||||||
|
}
|
||||||
|
if !plan.StorageProfile.IsNull() && !plan.StorageProfile.IsUnknown() {
|
||||||
|
v := plan.StorageProfile.ValueInt64()
|
||||||
|
createReq.StorageProfile = &v
|
||||||
|
}
|
||||||
|
if !plan.NetworkProfile.IsNull() && !plan.NetworkProfile.IsUnknown() {
|
||||||
|
v := plan.NetworkProfile.ValueInt64()
|
||||||
|
createReq.NetworkProfile = &v
|
||||||
|
}
|
||||||
|
if !plan.DryRun.IsNull() && !plan.DryRun.IsUnknown() {
|
||||||
|
v := plan.DryRun.ValueBool()
|
||||||
|
createReq.DryRun = &v
|
||||||
|
}
|
||||||
|
if !plan.AdditionalStorage1.IsNull() && !plan.AdditionalStorage1.IsUnknown() {
|
||||||
|
v := plan.AdditionalStorage1.ValueInt64()
|
||||||
|
createReq.AdditionalStorage1 = &v
|
||||||
|
}
|
||||||
|
if !plan.AdditionalStorage1Profile.IsNull() && !plan.AdditionalStorage1Profile.IsUnknown() {
|
||||||
|
v := plan.AdditionalStorage1Profile.ValueInt64()
|
||||||
|
createReq.AdditionalStorage1Profile = &v
|
||||||
|
}
|
||||||
|
if !plan.AdditionalStorage2.IsNull() && !plan.AdditionalStorage2.IsUnknown() {
|
||||||
|
v := plan.AdditionalStorage2.ValueInt64()
|
||||||
|
createReq.AdditionalStorage2 = &v
|
||||||
|
}
|
||||||
|
if !plan.AdditionalStorage2Profile.IsNull() && !plan.AdditionalStorage2Profile.IsUnknown() {
|
||||||
|
v := plan.AdditionalStorage2Profile.ValueInt64()
|
||||||
|
createReq.AdditionalStorage2Profile = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the server.
|
||||||
|
rawResp, err := r.client.Post(ctx, "/servers", createReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Creating Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the create response.
|
||||||
|
var serverResp client.ServerResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &serverResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set computed values from the response.
|
||||||
|
plan.ID = types.Int64Value(serverResp.Data.ID)
|
||||||
|
plan.UUID = types.StringValue(serverResp.Data.UUID)
|
||||||
|
plan.Hostname = types.StringValue(serverResp.Data.Hostname)
|
||||||
|
|
||||||
|
// If name was not set in the plan, use the name from the API response.
|
||||||
|
if plan.Name.IsNull() || plan.Name.IsUnknown() {
|
||||||
|
plan.Name = types.StringValue(serverResp.Data.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After creation, apply update-only attributes if they are set.
|
||||||
|
serverID := serverResp.Data.ID
|
||||||
|
|
||||||
|
// Apply name if explicitly set in the plan.
|
||||||
|
if !plan.Name.IsNull() && !plan.Name.IsUnknown() && plan.Name.ValueString() != serverResp.Data.Name {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/name", serverID), client.ServerModifyNameRequest{
|
||||||
|
Name: plan.Name.ValueString(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Setting Server Name", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply CPU throttle if set.
|
||||||
|
if !plan.CPUThrottle.IsNull() && !plan.CPUThrottle.IsUnknown() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuThrottle", serverID), client.ServerModifyCPUThrottleRequest{
|
||||||
|
Percentage: plan.CPUThrottle.ValueInt64(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Setting CPU Throttle", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply VNC if set.
|
||||||
|
if !plan.VNCEnabled.IsNull() && !plan.VNCEnabled.IsUnknown() && plan.VNCEnabled.ValueBool() {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/vnc", serverID), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Enabling VNC", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply suspended if set to true.
|
||||||
|
if !plan.Suspended.IsNull() && !plan.Suspended.IsUnknown() && plan.Suspended.ValueBool() {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/suspend", serverID), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Suspending Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply backup plan if set.
|
||||||
|
if !plan.BackupPlanID.IsNull() && !plan.BackupPlanID.IsUnknown() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/backups/plan/%d", serverID, plan.BackupPlanID.ValueInt64()), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Setting Backup Plan", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply custom XML if set.
|
||||||
|
if !plan.CustomXML.IsNull() && !plan.CustomXML.IsUnknown() {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/customXML", serverID), client.ServerCustomXMLRequest{
|
||||||
|
XML: plan.CustomXML.ValueString(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Setting Custom XML", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply owner change if set and different from the creating user.
|
||||||
|
if !plan.OwnerUserID.IsNull() && !plan.OwnerUserID.IsUnknown() && plan.OwnerUserID.ValueInt64() != plan.UserID.ValueInt64() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/owner/%d", serverID, plan.OwnerUserID.ValueInt64()), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Changing Server Owner", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state ServerResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResp, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d", state.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error Reading Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverResp client.ServerResponse
|
||||||
|
if err := json.Unmarshal(rawResp, &serverResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map API response to state model.
|
||||||
|
s := serverResp.Data
|
||||||
|
state.ID = types.Int64Value(s.ID)
|
||||||
|
state.UUID = types.StringValue(s.UUID)
|
||||||
|
state.Hostname = types.StringValue(s.Hostname)
|
||||||
|
state.Name = types.StringValue(s.Name)
|
||||||
|
state.HypervisorID = types.Int64Value(s.HypervisorID)
|
||||||
|
|
||||||
|
// Map optional create attributes from the nested API response if they were set in state.
|
||||||
|
if !state.Storage.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
|
||||||
|
state.Storage = types.Int64Value(s.Settings.Resources.Storage)
|
||||||
|
}
|
||||||
|
if !state.Memory.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
|
||||||
|
state.Memory = types.Int64Value(s.Settings.Resources.Memory)
|
||||||
|
}
|
||||||
|
if !state.Cores.IsNull() && s.CPU != nil {
|
||||||
|
state.Cores = types.Int64Value(s.CPU.Cores)
|
||||||
|
}
|
||||||
|
if !state.Traffic.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
|
||||||
|
state.Traffic = types.Int64Value(s.Settings.Resources.Traffic)
|
||||||
|
}
|
||||||
|
// NetworkSpeed and profiles are not returned by the detail API; preserve from state.
|
||||||
|
|
||||||
|
if !state.Suspended.IsNull() {
|
||||||
|
state.Suspended = types.BoolValue(s.Suspended)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan, state ServerResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := state.ID.ValueInt64()
|
||||||
|
|
||||||
|
// Preserve computed values from state.
|
||||||
|
plan.ID = state.ID
|
||||||
|
plan.UUID = state.UUID
|
||||||
|
plan.Hostname = state.Hostname
|
||||||
|
|
||||||
|
// Name change.
|
||||||
|
if !plan.Name.Equal(state.Name) {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/name", serverID), client.ServerModifyNameRequest{
|
||||||
|
Name: plan.Name.ValueString(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Modifying Server Name", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU cores change.
|
||||||
|
if !plan.Cores.Equal(state.Cores) && !plan.Cores.IsNull() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuCores", serverID), client.ServerModifyCPURequest{
|
||||||
|
CPUCores: plan.Cores.ValueInt64(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Modifying Server CPU Cores", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory change.
|
||||||
|
if !plan.Memory.Equal(state.Memory) && !plan.Memory.IsNull() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/memory", serverID), client.ServerModifyMemoryRequest{
|
||||||
|
Memory: plan.Memory.ValueInt64(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Modifying Server Memory", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traffic change.
|
||||||
|
if !plan.Traffic.Equal(state.Traffic) && !plan.Traffic.IsNull() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/traffic", serverID), client.ServerModifyTrafficRequest{
|
||||||
|
Traffic: plan.Traffic.ValueInt64(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Modifying Server Traffic", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU throttle change.
|
||||||
|
if !plan.CPUThrottle.Equal(state.CPUThrottle) && !plan.CPUThrottle.IsNull() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuThrottle", serverID), client.ServerModifyCPUThrottleRequest{
|
||||||
|
Percentage: plan.CPUThrottle.ValueInt64(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Modifying CPU Throttle", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VNC toggle.
|
||||||
|
if !plan.VNCEnabled.Equal(state.VNCEnabled) && !plan.VNCEnabled.IsNull() {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/vnc", serverID), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Toggling VNC", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suspended change.
|
||||||
|
if !plan.Suspended.Equal(state.Suspended) && !plan.Suspended.IsNull() {
|
||||||
|
if plan.Suspended.ValueBool() {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/suspend", serverID), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Suspending Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/unsuspend", serverID), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Unsuspending Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup plan change.
|
||||||
|
if !plan.BackupPlanID.Equal(state.BackupPlanID) {
|
||||||
|
planID := int64(0)
|
||||||
|
if !plan.BackupPlanID.IsNull() {
|
||||||
|
planID = plan.BackupPlanID.ValueInt64()
|
||||||
|
}
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/backups/plan/%d", serverID, planID), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Modifying Backup Plan", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom XML change.
|
||||||
|
if !plan.CustomXML.Equal(state.CustomXML) && !plan.CustomXML.IsNull() {
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/customXML", serverID), client.ServerCustomXMLRequest{
|
||||||
|
XML: plan.CustomXML.ValueString(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Setting Custom XML", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner change.
|
||||||
|
if !plan.OwnerUserID.Equal(state.OwnerUserID) && !plan.OwnerUserID.IsNull() {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/owner/%d", serverID, plan.OwnerUserID.ValueInt64()), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Changing Server Owner", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package change.
|
||||||
|
if !plan.PackageID.Equal(state.PackageID) {
|
||||||
|
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/package/%d", serverID, plan.PackageID.ValueInt64()), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error Changing Server Package", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state ServerResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.Delete(ctx, fmt.Sprintf("/servers/%d?delay=0", state.ID.ValueInt64()))
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
// Already deleted, nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error Deleting Server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
id, err := strconv.ParseInt(req.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Invalid Import ID",
|
||||||
|
fmt.Sprintf("Could not parse server ID %q as an integer: %s", req.ID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.Int64Value(id))...)
|
||||||
|
}
|
||||||
266
internal/provider/resource_server_build.go
Normal file
266
internal/provider/resource_server_build.go
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/booldefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerBuildResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerBuildResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerBuildResource creates a new server build resource.
|
||||||
|
func NewServerBuildResource() resource.Resource {
|
||||||
|
return &ServerBuildResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerBuildResource defines the resource implementation.
|
||||||
|
type ServerBuildResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerBuildResourceModel describes the resource data model.
|
||||||
|
type ServerBuildResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Hostname types.String `tfsdk:"hostname"`
|
||||||
|
OSID types.Int64 `tfsdk:"osid"`
|
||||||
|
VNC types.Bool `tfsdk:"vnc"`
|
||||||
|
Ipv6 types.Bool `tfsdk:"ipv6"`
|
||||||
|
SSHKeys types.List `tfsdk:"ssh_keys"`
|
||||||
|
Email types.Bool `tfsdk:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_build"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Builds a VirtFusion server with an operating system. This is a one-time operation — once a server is built, it stays built.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier of the server build (same as server_id).",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server to build.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The name for the server build.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"osid": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The operating system ID to install.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hostname": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The hostname for the server.",
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"vnc": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether to enable VNC access.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
PlanModifiers: []planmodifier.Bool{
|
||||||
|
boolplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipv6": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether to enable IPv6.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
PlanModifiers: []planmodifier.Bool{
|
||||||
|
boolplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ssh_keys": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of SSH key IDs to add to the server.",
|
||||||
|
Optional: true,
|
||||||
|
ElementType: types.Int64Type,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"email": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether to send a notification email after build.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
PlanModifiers: []planmodifier.Bool{
|
||||||
|
boolplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerBuildResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the API request body.
|
||||||
|
buildReq := client.ServerBuildRequest{
|
||||||
|
Name: data.Name.ValueString(),
|
||||||
|
OperatingSystemID: data.OSID.ValueInt64(),
|
||||||
|
VNC: data.VNC.ValueBool(),
|
||||||
|
Ipv6: data.Ipv6.ValueBool(),
|
||||||
|
Email: data.Email.ValueBool(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data.Hostname.IsNull() && !data.Hostname.IsUnknown() {
|
||||||
|
buildReq.Hostname = data.Hostname.ValueString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ssh_keys from types.List to []int64.
|
||||||
|
if !data.SSHKeys.IsNull() && !data.SSHKeys.IsUnknown() {
|
||||||
|
var sshKeys []int64
|
||||||
|
resp.Diagnostics.Append(data.SSHKeys.ElementsAs(ctx, &sshKeys, false)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buildReq.SSHKeys = sshKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/servers/%d/build", data.ServerID.ValueInt64())
|
||||||
|
_, err := r.client.Post(ctx, apiPath, buildReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Building Server",
|
||||||
|
fmt.Sprintf("Could not build server %d: %s", data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the ID to the server_id.
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d", data.ServerID.ValueInt64()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerBuildResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the server still exists.
|
||||||
|
apiPath := fmt.Sprintf("/servers/%d", data.ServerID.ValueInt64())
|
||||||
|
_, err := r.client.Get(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
// Server no longer exists, remove from state.
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Reading Server",
|
||||||
|
fmt.Sprintf("Could not read server %d: %s", data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data ServerBuildResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerBuildResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: building a server is not reversible. Removing from state only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *ServerBuildResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data ServerBuildResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate server_id is positive.
|
||||||
|
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("server_id"),
|
||||||
|
"Invalid Server ID",
|
||||||
|
"server_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate osid is positive.
|
||||||
|
if !data.OSID.IsNull() && !data.OSID.IsUnknown() && data.OSID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("osid"),
|
||||||
|
"Invalid OS ID",
|
||||||
|
"osid must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
317
internal/provider/resource_server_firewall.go
Normal file
317
internal/provider/resource_server_firewall.go
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"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/stringdefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerFirewallResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerFirewallResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerFirewallResource returns a new resource for managing server firewalls.
|
||||||
|
func NewServerFirewallResource() resource.Resource {
|
||||||
|
return &ServerFirewallResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerFirewallResource defines the resource implementation.
|
||||||
|
type ServerFirewallResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerFirewallResourceModel describes the resource data model.
|
||||||
|
type ServerFirewallResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
InterfaceName types.String `tfsdk:"interface_name"`
|
||||||
|
Rules types.List `tfsdk:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirewallRuleModel describes a single firewall rule.
|
||||||
|
type FirewallRuleModel struct {
|
||||||
|
Action types.String `tfsdk:"action"`
|
||||||
|
Direction types.String `tfsdk:"direction"`
|
||||||
|
Protocol types.String `tfsdk:"protocol"`
|
||||||
|
Port types.String `tfsdk:"port"`
|
||||||
|
IP types.String `tfsdk:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_firewall"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a VirtFusion server firewall.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Composite identifier in the format `server_id/interface_name`.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"interface_name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The network interface name. Defaults to `eth0`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: stringdefault.StaticString("eth0"),
|
||||||
|
},
|
||||||
|
"rules": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "The firewall rules.",
|
||||||
|
Optional: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"action": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The action for the rule (e.g. `accept`, `drop`).",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"direction": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The direction for the rule (e.g. `in`, `out`).",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"protocol": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The protocol for the rule (e.g. `tcp`, `udp`).",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"port": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The port or port range for the rule.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"ip": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IP address or CIDR for the rule.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerFirewallResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
|
// Enable the firewall
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/enable", serverID, iface), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error enabling server firewall", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set rules if provided
|
||||||
|
rules, diags := r.extractRules(ctx, data)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) > 0 {
|
||||||
|
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
|
||||||
|
_, err = r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error setting firewall rules", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerFirewallResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
|
result, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", serverID, iface))
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error reading server firewall", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fwResp client.FirewallResponse
|
||||||
|
if err := json.Unmarshal(result, &fwResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error parsing firewall response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the firewall is not enabled, remove from state
|
||||||
|
if !fwResp.Data.Enabled {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
|
||||||
|
|
||||||
|
// Map API rules to the model
|
||||||
|
ruleObjects := make([]attr.Value, len(fwResp.Data.Rules))
|
||||||
|
for i, rule := range fwResp.Data.Rules {
|
||||||
|
ruleObj, diags := types.ObjectValue(
|
||||||
|
firewallRuleAttrTypes(),
|
||||||
|
map[string]attr.Value{
|
||||||
|
"action": types.StringValue(rule.Action),
|
||||||
|
"direction": types.StringValue(rule.Direction),
|
||||||
|
"protocol": types.StringValue(rule.Protocol),
|
||||||
|
"port": types.StringValue(rule.Port),
|
||||||
|
"ip": types.StringValue(rule.IP),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ruleObjects[i] = ruleObj
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesList, diags := types.ListValue(types.ObjectType{AttrTypes: firewallRuleAttrTypes()}, ruleObjects)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Rules = rulesList
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data ServerFirewallResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
|
rules, diags := r.extractRules(ctx, data)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error updating firewall rules", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerFirewallResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data ServerFirewallResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/disable", serverID, iface), nil)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error disabling server firewall", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractRules converts the rules list from the model into client.FirewallRule slice.
|
||||||
|
func (r *ServerFirewallResource) extractRules(ctx context.Context, data ServerFirewallResourceModel) ([]client.FirewallRule, diag.Diagnostics) {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
|
if data.Rules.IsNull() || data.Rules.IsUnknown() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
var ruleModels []FirewallRuleModel
|
||||||
|
diags.Append(data.Rules.ElementsAs(ctx, &ruleModels, false)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := make([]client.FirewallRule, len(ruleModels))
|
||||||
|
for i, rm := range ruleModels {
|
||||||
|
rules[i] = client.FirewallRule{
|
||||||
|
Action: rm.Action.ValueString(),
|
||||||
|
Direction: rm.Direction.ValueString(),
|
||||||
|
Protocol: rm.Protocol.ValueString(),
|
||||||
|
Port: rm.Port.ValueString(),
|
||||||
|
IP: rm.IP.ValueString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// firewallRuleAttrTypes returns the attribute types for a firewall rule object.
|
||||||
|
func firewallRuleAttrTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"action": types.StringType,
|
||||||
|
"direction": types.StringType,
|
||||||
|
"protocol": types.StringType,
|
||||||
|
"port": types.StringType,
|
||||||
|
"ip": types.StringType,
|
||||||
|
}
|
||||||
|
}
|
||||||
160
internal/provider/resource_server_ipv4.go
Normal file
160
internal/provider/resource_server_ipv4.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerIPv4Resource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerIPv4Resource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerIPv4Resource returns a new resource for managing server IPv4 addresses.
|
||||||
|
func NewServerIPv4Resource() resource.Resource {
|
||||||
|
return &ServerIPv4Resource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerIPv4Resource defines the resource implementation.
|
||||||
|
type ServerIPv4Resource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerIPv4ResourceModel describes the resource data model.
|
||||||
|
type ServerIPv4ResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Quantity types.Int64 `tfsdk:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_ipv4"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Adds IPv4 addresses to a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Resource identifier.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server. Changing this forces a new resource to be created.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"quantity": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The number of IPv4 addresses to add. Defaults to `1`. Changing this forces a new resource to be created.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: int64default.StaticInt64(1),
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerIPv4ResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
quantity := data.Quantity.ValueInt64()
|
||||||
|
|
||||||
|
var body interface{}
|
||||||
|
if quantity > 1 {
|
||||||
|
body = client.ServerIPv4AddRequest{
|
||||||
|
Quantity: quantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/ipv4", serverID), body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error adding IPv4 to server", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/ipv4/%d", serverID, quantity))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerIPv4ResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No dedicated read endpoint; return stored state as-is.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
// All attributes have RequiresReplace, so Update should never be called.
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Update Not Supported",
|
||||||
|
"All attributes of virtfusion_server_ipv4 require replacement. This function should not be called.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerIPv4Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data ServerIPv4ResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
|
||||||
|
_, err := r.client.Delete(ctx, fmt.Sprintf("/servers/%d/ipv4", serverID))
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error removing IPv4 from server", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
158
internal/provider/resource_server_network_whitelist.go
Normal file
158
internal/provider/resource_server_network_whitelist.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerNetworkWhitelistResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerNetworkWhitelistResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerNetworkWhitelistResource returns a new resource for managing server network whitelist entries.
|
||||||
|
func NewServerNetworkWhitelistResource() resource.Resource {
|
||||||
|
return &ServerNetworkWhitelistResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerNetworkWhitelistResource defines the resource implementation.
|
||||||
|
type ServerNetworkWhitelistResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerNetworkWhitelistResourceModel describes the resource data model.
|
||||||
|
type ServerNetworkWhitelistResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
IP types.String `tfsdk:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_network_whitelist"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a VirtFusion server network whitelist entry.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Composite identifier in the format `server_id/ip`.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server. Changing this forces a new resource to be created.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ip": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IP address to whitelist. Changing this forces a new resource to be created.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerNetworkWhitelistResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
ip := data.IP.ValueString()
|
||||||
|
|
||||||
|
body := client.NetworkWhitelistRequest{
|
||||||
|
IP: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/networkWhitelist", serverID), body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error adding network whitelist entry", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, ip))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerNetworkWhitelistResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No dedicated read endpoint; return stored state as-is.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
// All attributes have RequiresReplace, so Update should never be called.
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Update Not Supported",
|
||||||
|
"All attributes of virtfusion_server_network_whitelist require replacement. This function should not be called.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerNetworkWhitelistResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data ServerNetworkWhitelistResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverID := data.ServerID.ValueInt64()
|
||||||
|
|
||||||
|
body := client.NetworkWhitelistRequest{
|
||||||
|
IP: data.IP.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.DeleteWithBody(ctx, fmt.Sprintf("/servers/%d/networkWhitelist", serverID), body)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error removing network whitelist entry", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
199
internal/provider/resource_server_password_reset.go
Normal file
199
internal/provider/resource_server_password_reset.go
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/mapplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerPasswordResetResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerPasswordResetResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerPasswordResetResource creates a new server password reset resource.
|
||||||
|
func NewServerPasswordResetResource() resource.Resource {
|
||||||
|
return &ServerPasswordResetResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerPasswordResetResource defines the resource implementation.
|
||||||
|
type ServerPasswordResetResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerPasswordResetResourceModel describes the resource data model.
|
||||||
|
type ServerPasswordResetResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
User types.String `tfsdk:"user"`
|
||||||
|
Password types.String `tfsdk:"password"`
|
||||||
|
Triggers types.Map `tfsdk:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_password_reset"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Resets the password for a VirtFusion server. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier for this password reset.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server to reset the password for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"user": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The user to reset the password for. Must be `root` (Linux) or `Administrator` (Windows).",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"password": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The new password generated by the reset operation.",
|
||||||
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"triggers": schema.MapAttribute{
|
||||||
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.Map{
|
||||||
|
mapplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]string{
|
||||||
|
"user": data.User.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/servers/%d/resetPassword", data.ServerID.ValueInt64())
|
||||||
|
rawResp, err := r.client.Post(ctx, apiPath, body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Resetting Server Password",
|
||||||
|
fmt.Sprintf("Could not reset password for server %d: %s", data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response for the new password.
|
||||||
|
if rawResp != nil {
|
||||||
|
var passResp client.PasswordResetResponse
|
||||||
|
if jsonErr := json.Unmarshal(rawResp, &passResp); jsonErr == nil && passResp.Data.Password != "" {
|
||||||
|
data.Password = types.StringValue(passResp.Data.Password)
|
||||||
|
} else {
|
||||||
|
data.Password = types.StringValue("")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.Password = types.StringValue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d-%d", data.ServerID.ValueInt64(), time.Now().UnixNano()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is for trigger-style resources.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data ServerPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: password resets are not reversible. Removing from state only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *ServerPasswordResetResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data ServerPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate server_id is positive.
|
||||||
|
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("server_id"),
|
||||||
|
"Invalid Server ID",
|
||||||
|
"server_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate user is one of the allowed values.
|
||||||
|
if !data.User.IsNull() && !data.User.IsUnknown() {
|
||||||
|
user := data.User.ValueString()
|
||||||
|
if user != "root" && user != "Administrator" {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("user"),
|
||||||
|
"Invalid User",
|
||||||
|
fmt.Sprintf("user must be either \"root\" or \"Administrator\". Got: %q", user),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
179
internal/provider/resource_server_power_action.go
Normal file
179
internal/provider/resource_server_power_action.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/mapplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerPowerActionResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerPowerActionResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerPowerActionResource creates a new server power action resource.
|
||||||
|
func NewServerPowerActionResource() resource.Resource {
|
||||||
|
return &ServerPowerActionResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerPowerActionResource defines the resource implementation.
|
||||||
|
type ServerPowerActionResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerPowerActionResourceModel describes the resource data model.
|
||||||
|
type ServerPowerActionResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Action types.String `tfsdk:"action"`
|
||||||
|
Triggers types.Map `tfsdk:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_power_action"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Performs a power action on a VirtFusion server. This is a trigger-style resource — the action is executed on create and can be re-triggered by changing the `triggers` attribute.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier for this power action.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server to perform the power action on.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"action": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The power action to perform. Must be one of: `boot`, `shutdown`, `restart`, `poweroff`.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"triggers": schema.MapAttribute{
|
||||||
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the power action to be re-executed. Works like `triggers` in `terraform_data`.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.Map{
|
||||||
|
mapplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerPowerActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/servers/%d/power/%s", data.ServerID.ValueInt64(), data.Action.ValueString())
|
||||||
|
_, err := r.client.Post(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Performing Server Power Action",
|
||||||
|
fmt.Sprintf("Could not perform power action %q on server %d: %s", data.Action.ValueString(), data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%d-%s-%d", data.ServerID.ValueInt64(), data.Action.ValueString(), time.Now().UnixNano()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerPowerActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is for trigger-style resources.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data ServerPowerActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerPowerActionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: power actions are not reversible. Removing from state only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *ServerPowerActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data ServerPowerActionResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate server_id is positive.
|
||||||
|
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("server_id"),
|
||||||
|
"Invalid Server ID",
|
||||||
|
"server_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate action is one of the allowed values.
|
||||||
|
if !data.Action.IsNull() && !data.Action.IsUnknown() {
|
||||||
|
action := data.Action.ValueString()
|
||||||
|
validActions := map[string]bool{
|
||||||
|
"boot": true,
|
||||||
|
"shutdown": true,
|
||||||
|
"restart": true,
|
||||||
|
"poweroff": true,
|
||||||
|
}
|
||||||
|
if !validActions[action] {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("action"),
|
||||||
|
"Invalid Power Action",
|
||||||
|
fmt.Sprintf("action must be one of: boot, shutdown, restart, poweroff. Got: %q", action),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
internal/provider/resource_server_traffic_block.go
Normal file
163
internal/provider/resource_server_traffic_block.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &ServerTrafficBlockResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ServerTrafficBlockResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServerTrafficBlockResource creates a new server traffic block resource.
|
||||||
|
func NewServerTrafficBlockResource() resource.Resource {
|
||||||
|
return &ServerTrafficBlockResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTrafficBlockResource defines the resource implementation.
|
||||||
|
type ServerTrafficBlockResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTrafficBlockResourceModel describes the resource data model.
|
||||||
|
type ServerTrafficBlockResourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_traffic_block"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a traffic block on a VirtFusion server.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The identifier of the traffic block.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server to add the traffic block to.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The type of traffic block (e.g. `inbound` or `outbound`).",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data ServerTrafficBlockResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blockReq := client.TrafficBlockRequest{
|
||||||
|
Type: data.Type.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/servers/%d/traffic/blocks", data.ServerID.ValueInt64())
|
||||||
|
respBody, err := r.client.Post(ctx, apiPath, blockReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating Traffic Block",
|
||||||
|
fmt.Sprintf("Could not create traffic block on server %d: %s", data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockResp client.TrafficBlockResponse
|
||||||
|
if err := json.Unmarshal(respBody, &blockResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Parsing Response",
|
||||||
|
fmt.Sprintf("Could not parse traffic block response: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(blockResp.Data.ID)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data ServerTrafficBlockResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
|
||||||
|
// All attributes require replacement — updates are never called.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServerTrafficBlockResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data ServerTrafficBlockResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/servers/%d/traffic/blocks/%d", data.ServerID.ValueInt64(), data.ID.ValueInt64())
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Deleting Traffic Block",
|
||||||
|
fmt.Sprintf("Could not delete traffic block %d on server %d: %s", data.ID.ValueInt64(), data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
226
internal/provider/resource_ssh_key.go
Normal file
226
internal/provider/resource_ssh_key.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/int64planmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &SSHKeyResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &SSHKeyResource{}
|
||||||
|
_ resource.ResourceWithImportState = &SSHKeyResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSSHKeyResource creates a new SSH key resource.
|
||||||
|
func NewSSHKeyResource() resource.Resource {
|
||||||
|
return &SSHKeyResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyResource defines the resource implementation.
|
||||||
|
type SSHKeyResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyResourceModel describes the resource data model.
|
||||||
|
type SSHKeyResourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
UserID types.Int64 `tfsdk:"user_id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
PublicKey types.String `tfsdk:"public_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_ssh_key"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a VirtFusion SSH key.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the SSH key.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the user who owns this SSH key.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The name of the SSH key.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"public_key": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The public key content.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data SSHKeyResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createReq := client.SSHKeyCreateRequest{
|
||||||
|
UserID: data.UserID.ValueInt64(),
|
||||||
|
Name: data.Name.ValueString(),
|
||||||
|
PublicKey: data.PublicKey.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := r.client.Post(ctx, "/ssh_keys", createReq)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Creating SSH Key",
|
||||||
|
fmt.Sprintf("Could not create SSH key: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshKeyResp client.SSHKeyResponse
|
||||||
|
if err := json.Unmarshal(respBody, &sshKeyResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Parsing Response",
|
||||||
|
fmt.Sprintf("Could not parse SSH key response: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(sshKeyResp.Data.ID)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data SSHKeyResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64())
|
||||||
|
respBody, err := r.client.Get(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
// SSH key no longer exists, remove from state.
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Reading SSH Key",
|
||||||
|
fmt.Sprintf("Could not read SSH key %d: %s", data.ID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshKeyResp client.SSHKeyResponse
|
||||||
|
if err := json.Unmarshal(respBody, &sshKeyResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Parsing Response",
|
||||||
|
fmt.Sprintf("Could not parse SSH key response: %s", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state from API response.
|
||||||
|
data.Name = types.StringValue(sshKeyResp.Data.Name)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data SSHKeyResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data SSHKeyResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64())
|
||||||
|
_, err := r.client.Delete(ctx, apiPath)
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
// Already deleted, nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Deleting SSH Key",
|
||||||
|
fmt.Sprintf("Could not delete SSH key %d: %s", data.ID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
id, err := strconv.ParseInt(req.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Invalid Import ID",
|
||||||
|
fmt.Sprintf("Could not parse SSH key ID %q as integer: %s", req.ID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.Int64Value(id))...)
|
||||||
|
}
|
||||||
218
internal/provider/resource_user.go
Normal file
218
internal/provider/resource_user.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &UserResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &UserResource{}
|
||||||
|
_ resource.ResourceWithImportState = &UserResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUserResource returns a new resource for managing VirtFusion users.
|
||||||
|
func NewUserResource() resource.Resource {
|
||||||
|
return &UserResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResource defines the resource implementation.
|
||||||
|
type UserResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResourceModel describes the resource data model.
|
||||||
|
type UserResourceModel struct {
|
||||||
|
ID types.Int64 `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Email types.String `tfsdk:"email"`
|
||||||
|
ExtRelationID types.String `tfsdk:"ext_relation_id"`
|
||||||
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_user"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Manages a VirtFusion user.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The numeric ID of the user.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The name of the user.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"email": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The email address of the user.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"ext_relation_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The external relation ID used to look up the user. Changing this forces a new resource to be created.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether the user is enabled.",
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data UserResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := client.UserCreateRequest{
|
||||||
|
Name: data.Name.ValueString(),
|
||||||
|
Email: data.Email.ValueString(),
|
||||||
|
ExtRelationID: data.ExtRelationID.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.client.Post(ctx, "/users", body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error creating user", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userResp client.UserResponse
|
||||||
|
if err := json.Unmarshal(result, &userResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error parsing user response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(userResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(userResp.Data.Name)
|
||||||
|
data.Email = types.StringValue(userResp.Data.Email)
|
||||||
|
data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
|
||||||
|
data.Enabled = types.BoolValue(userResp.Data.Enabled)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data UserResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error reading user", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userResp client.UserResponse
|
||||||
|
if err := json.Unmarshal(result, &userResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error parsing user response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(userResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(userResp.Data.Name)
|
||||||
|
data.Email = types.StringValue(userResp.Data.Email)
|
||||||
|
data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
|
||||||
|
data.Enabled = types.BoolValue(userResp.Data.Enabled)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data UserResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := client.UserModifyRequest{
|
||||||
|
Name: data.Name.ValueString(),
|
||||||
|
Email: data.Email.ValueString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.client.Put(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()), body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error updating user", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userResp client.UserResponse
|
||||||
|
if err := json.Unmarshal(result, &userResp); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error parsing user response", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.Int64Value(userResp.Data.ID)
|
||||||
|
data.Name = types.StringValue(userResp.Data.Name)
|
||||||
|
data.Email = types.StringValue(userResp.Data.Email)
|
||||||
|
data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
|
||||||
|
data.Enabled = types.BoolValue(userResp.Data.Enabled)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data UserResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.client.Delete(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
|
||||||
|
if err != nil {
|
||||||
|
var apiErr *client.APIError
|
||||||
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Error deleting user", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
resource.ImportStatePassthroughID(ctx, path.Root("ext_relation_id"), req, resp)
|
||||||
|
}
|
||||||
170
internal/provider/resource_user_auth_token.go
Normal file
170
internal/provider/resource_user_auth_token.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &UserAuthTokenResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &UserAuthTokenResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUserAuthTokenResource creates a new user auth token resource.
|
||||||
|
func NewUserAuthTokenResource() resource.Resource {
|
||||||
|
return &UserAuthTokenResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAuthTokenResource defines the resource implementation.
|
||||||
|
type UserAuthTokenResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAuthTokenResourceModel describes the resource data model.
|
||||||
|
type UserAuthTokenResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ExtRelationID types.String `tfsdk:"ext_relation_id"`
|
||||||
|
Token types.String `tfsdk:"token"`
|
||||||
|
URL types.String `tfsdk:"url"`
|
||||||
|
Triggers types.Map `tfsdk:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_user_auth_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Generates an authentication token for a VirtFusion user. This is a trigger-style resource — the token is generated on create and can be re-generated by changing the `triggers` attribute.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier for this auth token generation.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ext_relation_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The external relation ID of the user to generate the auth token for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"token": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The generated authentication token.",
|
||||||
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"url": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The authentication URL for the generated token.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"triggers": schema.MapAttribute{
|
||||||
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the auth token to be re-generated. Works like `triggers` in `terraform_data`.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.Map{
|
||||||
|
mapplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data UserAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/users/%s/authenticationTokens", data.ExtRelationID.ValueString())
|
||||||
|
rawResp, err := r.client.Post(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Generating User Auth Token",
|
||||||
|
fmt.Sprintf("Could not generate auth token for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response for the token and URL.
|
||||||
|
if rawResp != nil {
|
||||||
|
var tokenResp client.AuthTokenResponse
|
||||||
|
if jsonErr := json.Unmarshal(rawResp, &tokenResp); jsonErr == nil {
|
||||||
|
data.Token = types.StringValue(tokenResp.Data.Token)
|
||||||
|
data.URL = types.StringValue(tokenResp.Data.URL)
|
||||||
|
} else {
|
||||||
|
data.Token = types.StringValue("")
|
||||||
|
data.URL = types.StringValue("")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.Token = types.StringValue("")
|
||||||
|
data.URL = types.StringValue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data UserAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is for trigger-style resources.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data UserAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserAuthTokenResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: auth tokens cannot be revoked via this resource. Removing from state only.
|
||||||
|
}
|
||||||
137
internal/provider/resource_user_password_reset.go
Normal file
137
internal/provider/resource_user_password_reset.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &UserPasswordResetResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &UserPasswordResetResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUserPasswordResetResource creates a new user password reset resource.
|
||||||
|
func NewUserPasswordResetResource() resource.Resource {
|
||||||
|
return &UserPasswordResetResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPasswordResetResource defines the resource implementation.
|
||||||
|
type UserPasswordResetResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPasswordResetResourceModel describes the resource data model.
|
||||||
|
type UserPasswordResetResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ExtRelationID types.String `tfsdk:"ext_relation_id"`
|
||||||
|
Triggers types.Map `tfsdk:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_user_password_reset"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Resets the password for a VirtFusion user by external relation ID. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier for this password reset.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ext_relation_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The external relation ID of the user to reset the password for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"triggers": schema.MapAttribute{
|
||||||
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.Map{
|
||||||
|
mapplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data UserPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/users/%s/byExtRelation/resetPassword", data.ExtRelationID.ValueString())
|
||||||
|
_, err := r.client.Post(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Resetting User Password",
|
||||||
|
fmt.Sprintf("Could not reset password for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data UserPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is for trigger-style resources.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data UserPasswordResetResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: password resets are not reversible. Removing from state only.
|
||||||
|
}
|
||||||
194
internal/provider/resource_user_server_auth_token.go
Normal file
194
internal/provider/resource_user_server_auth_token.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// Copyright (c) EZSCALE.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
|
"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/mapplanmodifier"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &UserServerAuthTokenResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &UserServerAuthTokenResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUserServerAuthTokenResource creates a new user server auth token resource.
|
||||||
|
func NewUserServerAuthTokenResource() resource.Resource {
|
||||||
|
return &UserServerAuthTokenResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserServerAuthTokenResource defines the resource implementation.
|
||||||
|
type UserServerAuthTokenResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserServerAuthTokenResourceModel describes the resource data model.
|
||||||
|
type UserServerAuthTokenResourceModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
ExtRelationID types.String `tfsdk:"ext_relation_id"`
|
||||||
|
ServerID types.Int64 `tfsdk:"server_id"`
|
||||||
|
Token types.String `tfsdk:"token"`
|
||||||
|
URL types.String `tfsdk:"url"`
|
||||||
|
Triggers types.Map `tfsdk:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_user_server_auth_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Generates a server-scoped authentication token for a VirtFusion user. This is a trigger-style resource — the token is generated on create and can be re-generated by changing the `triggers` attribute.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The identifier for this server auth token generation.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ext_relation_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The external relation ID of the user to generate the server auth token for.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "The ID of the server to scope the auth token to.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"token": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The generated server authentication token.",
|
||||||
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"url": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The authentication URL for the generated server token.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"triggers": schema.MapAttribute{
|
||||||
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the server auth token to be re-generated. Works like `triggers` in `terraform_data`.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.Map{
|
||||||
|
mapplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := req.ProviderData.(*client.Client)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data UserServerAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPath := fmt.Sprintf("/users/%s/serverAuthenticationTokens/%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64())
|
||||||
|
rawResp, err := r.client.Post(ctx, apiPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error Generating User Server Auth Token",
|
||||||
|
fmt.Sprintf("Could not generate server auth token for user %q on server %d: %s", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64(), err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response for the token and URL.
|
||||||
|
if rawResp != nil {
|
||||||
|
var tokenResp client.AuthTokenResponse
|
||||||
|
if jsonErr := json.Unmarshal(rawResp, &tokenResp); jsonErr == nil {
|
||||||
|
data.Token = types.StringValue(tokenResp.Data.Token)
|
||||||
|
data.URL = types.StringValue(tokenResp.Data.URL)
|
||||||
|
} else {
|
||||||
|
data.Token = types.StringValue("")
|
||||||
|
data.URL = types.StringValue("")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.Token = types.StringValue("")
|
||||||
|
data.URL = types.StringValue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ID = types.StringValue(fmt.Sprintf("%s-%d-%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64(), time.Now().UnixNano()))
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data UserServerAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stored state as-is for trigger-style resources.
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data UserServerAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserServerAuthTokenResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||||
|
// No-op: server auth tokens cannot be revoked via this resource. Removing from state only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the resource configuration.
|
||||||
|
func (r *UserServerAuthTokenResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
var data UserServerAuthTokenResourceModel
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate server_id is positive.
|
||||||
|
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
path.Root("server_id"),
|
||||||
|
"Invalid Server ID",
|
||||||
|
"server_id must be a positive integer.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package provider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure provider defined types fully satisfy framework interfaces.
|
|
||||||
var _ resource.Resource = &VirtfusionServerBuildResource{}
|
|
||||||
var _ resource.ResourceWithImportState = &VirtfusionServerBuildResource{}
|
|
||||||
|
|
||||||
func NewVirtfusionServerBuildResource() resource.Resource {
|
|
||||||
return &VirtfusionServerBuildResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VirtfusionServerBuildResource defines the resource implementation.
|
|
||||||
type VirtfusionServerBuildResource struct {
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type VirtfusionServerBuildResourceModel struct {
|
|
||||||
ServerId int64 `tfsdk:"server_id"`
|
|
||||||
Name string `tfsdk:"name" json:"name"`
|
|
||||||
Hostname string `tfsdk:"hostname" json:"hostname"`
|
|
||||||
Osid int64 `tfsdk:"osid" json:"operatingSystemId"`
|
|
||||||
Vnc bool `tfsdk:"vnc" json:"vnc"`
|
|
||||||
Ipv6 bool `tfsdk:"ipv6" json:"ipv6"`
|
|
||||||
SshKeys []int64 `tfsdk:"ssh_keys" json:"sshKeys"`
|
|
||||||
Email bool `tfsdk:"email" json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_build"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
// This description is used by the documentation generator and the language server.
|
|
||||||
MarkdownDescription: "Virtfusion Server Build Resource",
|
|
||||||
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"server_id": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Server ID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
MarkdownDescription: "Server Name",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"hostname": schema.StringAttribute{
|
|
||||||
MarkdownDescription: "Server Hostname",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"osid": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Server Operating System ID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"vnc": schema.BoolAttribute{
|
|
||||||
MarkdownDescription: "Server VNC",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Default: booldefault.StaticBool(false),
|
|
||||||
},
|
|
||||||
"ipv6": schema.BoolAttribute{
|
|
||||||
MarkdownDescription: "Server IPv6",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Default: booldefault.StaticBool(false),
|
|
||||||
},
|
|
||||||
"ssh_keys": schema.ListAttribute{
|
|
||||||
MarkdownDescription: "Server SSH Keys IDs",
|
|
||||||
ElementType: types.Int64Type,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"email": schema.BoolAttribute{
|
|
||||||
MarkdownDescription: "Server Email",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Default: booldefault.StaticBool(false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) 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.(*http.Client)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Unexpected Resource Configure Type",
|
|
||||||
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
||||||
var data VirtfusionServerBuildResourceModel
|
|
||||||
|
|
||||||
// Read Terraform plan data into the model
|
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createReq := VirtfusionServerBuildResourceModel{
|
|
||||||
Name: data.Name,
|
|
||||||
Hostname: data.Hostname,
|
|
||||||
Osid: data.Osid,
|
|
||||||
Vnc: data.Vnc,
|
|
||||||
Ipv6: data.Ipv6,
|
|
||||||
SshKeys: data.SshKeys,
|
|
||||||
Email: data.Email,
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReqBody, err := json.Marshal(createReq)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Unable to Create Resource",
|
|
||||||
"An unexpected error occurred while creating the resource create request. "+
|
|
||||||
"Please report this issue to the provider developers.\n\n"+
|
|
||||||
"JSON Error: "+err.Error(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequest("POST", fmt.Sprintf("/servers/%d/build", data.ServerId), bytes.NewBuffer(httpReqBody))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Request",
|
|
||||||
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any additional headers (Content-Type, etc.)
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
httpResponse, err := r.client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Execute Request",
|
|
||||||
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Close Request",
|
|
||||||
fmt.Sprintf("Failed to close HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(httpResponse.Body)
|
|
||||||
|
|
||||||
if httpResponse.StatusCode == 422 {
|
|
||||||
responseBody, err := ioutil.ReadAll(httpResponse.Body)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Read Response",
|
|
||||||
fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorResponse map[string]interface{}
|
|
||||||
err = json.Unmarshal(responseBody, &errorResponse)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Parse Error Response",
|
|
||||||
fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors, exists := errorResponse["errors"]; exists {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Server Returned Errors",
|
|
||||||
fmt.Sprintf("Errors from server: %v", errors),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpResponse.StatusCode != 200 {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Resource",
|
|
||||||
fmt.Sprintf("Failed to create resource: %s", httpResponse.Status),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
||||||
var data VirtfusionServerBuildResourceModel
|
|
||||||
|
|
||||||
// Read Terraform prior state data into the model
|
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If applicable, this is a great opportunity to initialize any necessary
|
|
||||||
// provider client data and make a call using it.
|
|
||||||
// httpResp, err := r.client.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
||||||
var data VirtfusionServerBuildResourceModel
|
|
||||||
|
|
||||||
// Read Terraform plan data into the model
|
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If applicable, this is a great opportunity to initialize any necessary
|
|
||||||
// provider client data and make a call using it.
|
|
||||||
// httpResp, err := r.client.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
||||||
var data VirtfusionServerBuildResourceModel
|
|
||||||
|
|
||||||
// Read Terraform prior state data into the model
|
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerBuildResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
||||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
|
||||||
}
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package provider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure provider defined types fully satisfy framework interfaces.
|
|
||||||
var _ resource.Resource = &VirtfusionServerResource{}
|
|
||||||
var _ resource.ResourceWithImportState = &VirtfusionServerResource{}
|
|
||||||
|
|
||||||
func NewVirtfusionServerResource() resource.Resource {
|
|
||||||
return &VirtfusionServerResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VirtfusionServerResource defines the resource implementation.
|
|
||||||
type VirtfusionServerResource struct {
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// VirtfusionServerResourceModel describes the resource data model.
|
|
||||||
type VirtfusionServerResourceModel struct {
|
|
||||||
PackageId *int64 `tfsdk:"package_id" json:"packageId,omitempty"`
|
|
||||||
UserId *int64 `tfsdk:"user_id" json:"userId,omitempty"`
|
|
||||||
HypervisorId *int64 `tfsdk:"hypervisor_id" json:"hypervisorId,omitempty"`
|
|
||||||
Ipv4 *int64 `tfsdk:"ipv4" json:"ipv4,omitempty"`
|
|
||||||
Storage *int64 `tfsdk:"storage" json:"storage,omitempty"`
|
|
||||||
Memory *int64 `tfsdk:"memory" json:"memory,omitempty"`
|
|
||||||
Cores *int64 `tfsdk:"cores" json:"cpuCores,omitempty"`
|
|
||||||
Traffic *int64 `tfsdk:"traffic" json:"traffic,omitempty"`
|
|
||||||
InboundNetworkSpeed *int64 `tfsdk:"inbound_network_speed" json:"networkSpeedInbound,omitempty"`
|
|
||||||
OutboundNetworkSpeed *int64 `tfsdk:"outbound_network_speed" json:"networkSpeedOutbound,omitempty"`
|
|
||||||
StorageProfile *int64 `tfsdk:"storage_profile" json:"storageProfile,omitempty"`
|
|
||||||
NetworkProfile *int64 `tfsdk:"network_profile" json:"networkProfile,omitempty"`
|
|
||||||
Id types.Int64 `tfsdk:"id" json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_server"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
// This description is used by the documentation generator and the language server.
|
|
||||||
MarkdownDescription: "Virtfusion Server Resource",
|
|
||||||
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"package_id": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Package ID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"user_id": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "User ID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"hypervisor_id": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Hypervisor Group ID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"ipv4": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "IPv4 Addresses to assign. Omit to use the default of 1 IPv4.",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Default: int64default.StaticInt64(1),
|
|
||||||
},
|
|
||||||
"storage": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Primary storage size in GB. Omit to use the default storage size from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"memory": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "How much memory to allocate in MB. Omit to use the default memory size from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"cores": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "How many cores to allocate. Omit to use the default core count from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"traffic": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "How much traffic to allocate in GB. Omit to use the default traffic size from the package. 0=Unlimited",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"inbound_network_speed": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Inbound network speed in kB/s. Omit to use the default inbound network speed from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"outbound_network_speed": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Outbound network speed in kB/s. Omit to use the default outbound network speed from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"storage_profile": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Storage profile ID. Omit to use the default storage profile from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"network_profile": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Network profile ID. Omit to use the default network profile from the package.",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"id": schema.Int64Attribute{
|
|
||||||
MarkdownDescription: "Server ID",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) 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.(*http.Client)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Unexpected Resource Configure Type",
|
|
||||||
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
||||||
var data VirtfusionServerResourceModel
|
|
||||||
|
|
||||||
// Read Terraform plan data into the model
|
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createReq := VirtfusionServerResourceModel{
|
|
||||||
PackageId: data.PackageId,
|
|
||||||
UserId: data.UserId,
|
|
||||||
HypervisorId: data.HypervisorId,
|
|
||||||
Ipv4: data.Ipv4,
|
|
||||||
Storage: data.Storage,
|
|
||||||
Traffic: data.Traffic,
|
|
||||||
Memory: data.Memory,
|
|
||||||
Cores: data.Cores,
|
|
||||||
InboundNetworkSpeed: data.InboundNetworkSpeed,
|
|
||||||
OutboundNetworkSpeed: data.OutboundNetworkSpeed,
|
|
||||||
StorageProfile: data.StorageProfile,
|
|
||||||
NetworkProfile: data.NetworkProfile,
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReqBody, err := json.Marshal(createReq)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Unable to Create Resource",
|
|
||||||
"An unexpected error occurred while creating the resource create request. "+
|
|
||||||
"Please report this issue to the provider developers.\n\n"+
|
|
||||||
"JSON Error: "+err.Error(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequest("POST", "/servers", bytes.NewBuffer(httpReqBody))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Request",
|
|
||||||
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any additional headers (Content-Type, etc.)
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
httpResponse, err := r.client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Execute Request",
|
|
||||||
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Close Request",
|
|
||||||
fmt.Sprintf("Failed to close HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(httpResponse.Body)
|
|
||||||
|
|
||||||
if httpResponse.StatusCode == 422 {
|
|
||||||
responseBody, err := ioutil.ReadAll(httpResponse.Body)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Read Response",
|
|
||||||
fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorResponse map[string]interface{}
|
|
||||||
err = json.Unmarshal(responseBody, &errorResponse)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Parse Error Response",
|
|
||||||
fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors, exists := errorResponse["errors"]; exists {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Server Returned Errors",
|
|
||||||
fmt.Sprintf("Errors from server: %v", errors),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpResponse.StatusCode != 201 {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Resource",
|
|
||||||
fmt.Sprintf("Failed to create resource: %s", httpResponse.Status),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBody, err := ioutil.ReadAll(httpResponse.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Read Response",
|
|
||||||
fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseData struct {
|
|
||||||
Data struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseData ResponseData
|
|
||||||
|
|
||||||
// Unmarshal the JSON response
|
|
||||||
err = json.Unmarshal(responseBody, &responseData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Parse Response",
|
|
||||||
fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the Terraform state with the server ID
|
|
||||||
data.Id = types.Int64Value(responseData.Data.Id)
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
||||||
var data VirtfusionServerResourceModel
|
|
||||||
|
|
||||||
// Read Terraform prior state data into the model
|
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If applicable, this is a great opportunity to initialize any necessary
|
|
||||||
// provider client data and make a call using it.
|
|
||||||
// httpResp, err := r.client.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
||||||
var data VirtfusionServerResourceModel
|
|
||||||
|
|
||||||
// Read Terraform plan data into the model
|
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If applicable, this is a great opportunity to initialize any necessary
|
|
||||||
// provider client data and make a call using it.
|
|
||||||
// httpResp, err := r.client.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
||||||
var data VirtfusionServerResourceModel
|
|
||||||
|
|
||||||
// Read Terraform prior state data into the model
|
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequest("DELETE", fmt.Sprintf("/servers/%d?delay=0", data.Id.ValueInt64()), bytes.NewBuffer([]byte{}))
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Request",
|
|
||||||
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any additional headers (Content-Type, etc.)
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
httpResponse, err := r.client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Execute Request",
|
|
||||||
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpResponse.StatusCode != 204 {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Delete Resource",
|
|
||||||
fmt.Sprintf("Failed to delete resource: %s", httpResponse.Status),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
||||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
|
||||||
}
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package provider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"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/types"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure provider defined types fully satisfy framework interfaces.
|
|
||||||
var _ resource.Resource = &VirtfusionSSHResource{}
|
|
||||||
var _ resource.ResourceWithImportState = &VirtfusionSSHResource{}
|
|
||||||
|
|
||||||
func NewVirtfusionSSHResource() resource.Resource {
|
|
||||||
return &VirtfusionSSHResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VirtfusionSSHResource defines the resource implementation.
|
|
||||||
type VirtfusionSSHResource struct {
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// VirtfusionSSHResourceModel describes the resource data model.
|
|
||||||
type VirtfusionSSHResourceModel struct {
|
|
||||||
UserId *int64 `tfsdk:"user_id" json:"userId"`
|
|
||||||
Name *string `tfsdk:"name" json:"name"`
|
|
||||||
PublicKey *string `tfsdk:"public_key" json:"publicKey"`
|
|
||||||
Id types.Int64 `tfsdk:"id" json:"id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_ssh"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
// This description is used by the documentation generator and the language server.
|
|
||||||
MarkdownDescription: "Virtfusion SSH Resource",
|
|
||||||
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"user_id": schema.Int64Attribute{
|
|
||||||
Description: "User ID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Description: "Key Name",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"public_key": schema.StringAttribute{
|
|
||||||
Description: "Public Key",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"id": schema.Int64Attribute{
|
|
||||||
Description: "SSH Key ID",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) 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.(*http.Client)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Unexpected Resource Configure Type",
|
|
||||||
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
||||||
var data VirtfusionSSHResourceModel
|
|
||||||
|
|
||||||
// Read Terraform plan data into the model
|
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createReq := VirtfusionSSHResourceModel{
|
|
||||||
UserId: data.UserId,
|
|
||||||
Name: data.Name,
|
|
||||||
PublicKey: data.PublicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the model to JSON
|
|
||||||
jsonReq, err := json.Marshal(createReq)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to marshal request body",
|
|
||||||
fmt.Sprintf("Failed to marshal request body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := r.client.Post("/ssh_keys", "application/json", bytes.NewBuffer(jsonReq))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Request failed",
|
|
||||||
fmt.Sprintf("Request failed: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to close response body",
|
|
||||||
fmt.Sprintf("Failed to close response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}(httpReq.Body)
|
|
||||||
|
|
||||||
if httpReq.StatusCode != 201 {
|
|
||||||
|
|
||||||
if httpReq.StatusCode == 422 {
|
|
||||||
responseBody, _ := io.ReadAll(httpReq.Body)
|
|
||||||
var errorResponse map[string]interface{}
|
|
||||||
err = json.Unmarshal(responseBody, &errorResponse)
|
|
||||||
if errors, exists := errorResponse["errors"]; exists {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to create SSH key",
|
|
||||||
fmt.Sprintf("Errors from server: %v", errors),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Invalid Request",
|
|
||||||
fmt.Sprintf("Failed to create SSH key: %s", httpReq.Status),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the response body into the model. The response is expected to be a JSON object with the body of the created
|
|
||||||
// ssh key within the `data` field. The `data` field is a JSON object with the ssh key data.
|
|
||||||
responseBody, err := io.ReadAll(httpReq.Body)
|
|
||||||
|
|
||||||
type ResponseData struct {
|
|
||||||
Data struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseData ResponseData
|
|
||||||
|
|
||||||
// Unmarshal the response body into the model
|
|
||||||
err = json.Unmarshal(responseBody, &responseData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to unmarshal response body",
|
|
||||||
fmt.Sprintf("Failed to unmarshal response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Id = types.Int64Value(responseData.Data.Id)
|
|
||||||
data.Name = &responseData.Data.Name
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
||||||
var data VirtfusionSSHResourceModel
|
|
||||||
|
|
||||||
// Read Terraform prior state data into the model
|
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequest("GET", fmt.Sprintf("/ssh_keys/%d", data.Id.ValueInt64()), nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Request",
|
|
||||||
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the resource returns a 404, then the resource has been deleted. Return an empty state.
|
|
||||||
httpResponse, err := r.client.Do(httpReq)
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to close response body",
|
|
||||||
fmt.Sprintf("Failed to close response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}(httpResponse.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Execute Request",
|
|
||||||
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpResponse.StatusCode == 404 {
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseData struct {
|
|
||||||
Data struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
CreatedAt string `json:"created"`
|
|
||||||
UpdatedAt string `json:"updated"`
|
|
||||||
PublicKeyHash string `json:"publicKey"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(httpResponse.Body).Decode(&responseData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to decode response body",
|
|
||||||
fmt.Sprintf("Failed to decode response body: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Name = &responseData.Data.Name
|
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
||||||
var data VirtfusionSSHResourceModel
|
|
||||||
|
|
||||||
// Read Terraform plan data into the model
|
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
||||||
var data VirtfusionSSHResourceModel
|
|
||||||
|
|
||||||
// Read Terraform prior state data into the model
|
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
||||||
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequest("DELETE", fmt.Sprintf("/ssh_keys/%d", data.Id.ValueInt64()), nil)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Create Request",
|
|
||||||
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any additional headers (Content-Type, etc.)
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
httpResponse, err := r.client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Execute Request",
|
|
||||||
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpResponse.StatusCode != 204 {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Failed to Delete Resource",
|
|
||||||
fmt.Sprintf("Failed to delete resource: %s", httpResponse.Status),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.Diagnostics.AddError(
|
|
||||||
"Request failed",
|
|
||||||
fmt.Sprintf("Request failed: %s", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *VirtfusionSSHResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
||||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
|
||||||
}
|
|
||||||
14
main.go
14
main.go
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) EZSCALE.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -15,21 +15,19 @@ import (
|
|||||||
|
|
||||||
// Run "go generate" to format example terraform files and generate the docs for the registry/website
|
// 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
|
// If you do not have terraform installed, you can remove the formatting command, but it is suggested to
|
||||||
// ensure the documentation is formatted properly.
|
// ensure the documentation is formatted properly.
|
||||||
//go:generate terraform fmt -recursive ./examples/
|
//go:generate terraform fmt -recursive ./examples/
|
||||||
|
|
||||||
// Run the docs generation tool, check its repository for more information on how it works and how docs
|
// Run the docs generation tool, check its repository for more information on how it works and how docs
|
||||||
// can be customized.
|
// can be customized.
|
||||||
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
|
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate -provider-name virtfusion
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// these will be set by the goreleaser configuration
|
// These will be set by the goreleaser configuration
|
||||||
// to appropriate values for the compiled binary.
|
// to appropriate values for the compiled binary.
|
||||||
version string = "0.0.3"
|
|
||||||
|
|
||||||
// goreleaser can pass other information to the main package, such as the specific commit
|
|
||||||
// https://goreleaser.com/cookbooks/using-main.version/
|
// https://goreleaser.com/cookbooks/using-main.version/
|
||||||
|
version string = "dev"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -39,13 +37,11 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
opts := providerserver.ServeOpts{
|
opts := providerserver.ServeOpts{
|
||||||
// TODO: Update this string with the published name of your provider.
|
|
||||||
Address: "registry.terraform.io/EZSCALE/virtfusion",
|
Address: "registry.terraform.io/EZSCALE/virtfusion",
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := providerserver.Serve(context.Background(), provider.New(version), opts)
|
err := providerserver.Serve(context.Background(), provider.New(version), opts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
8309
openapi.yaml
Normal file
8309
openapi.yaml
Normal file
File diff suppressed because one or more lines are too long
154
scripts/check-endpoint-drift.go
Normal file
154
scripts/check-endpoint-drift.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
// check-endpoint-drift compares the current OpenAPI spec against the endpoint manifest
|
||||||
|
// and reports any added or removed endpoints. Exit 0 if no drift, exit 1 if drift detected.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint struct {
|
||||||
|
Method string `json:"method" yaml:"-"`
|
||||||
|
Path string `json:"path" yaml:"-"`
|
||||||
|
Summary string `json:"summary" yaml:"-"`
|
||||||
|
Tag string `json:"tag" yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointKey(e Endpoint) string {
|
||||||
|
return e.Method + " " + e.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAPISpec struct {
|
||||||
|
Paths map[string]map[string]struct {
|
||||||
|
Summary string `yaml:"summary"`
|
||||||
|
Tags []string `yaml:"tags"`
|
||||||
|
} `yaml:"paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEndpoints(specPath string) ([]Endpoint, error) {
|
||||||
|
data, err := os.ReadFile(specPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var spec OpenAPISpec
|
||||||
|
if err := yaml.Unmarshal(data, &spec); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
validMethods := map[string]bool{
|
||||||
|
"get": true, "post": true, "put": true, "delete": true, "patch": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoints []Endpoint
|
||||||
|
for path, methods := range spec.Paths {
|
||||||
|
for method, details := range methods {
|
||||||
|
if !validMethods[strings.ToLower(method)] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tag := "Untagged"
|
||||||
|
if len(details.Tags) > 0 {
|
||||||
|
tag = details.Tags[0]
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, Endpoint{
|
||||||
|
Method: strings.ToUpper(method),
|
||||||
|
Path: path,
|
||||||
|
Summary: details.Summary,
|
||||||
|
Tag: tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(endpoints, func(i, j int) bool {
|
||||||
|
if endpoints[i].Path != endpoints[j].Path {
|
||||||
|
return endpoints[i].Path < endpoints[j].Path
|
||||||
|
}
|
||||||
|
return endpoints[i].Method < endpoints[j].Method
|
||||||
|
})
|
||||||
|
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Resolve paths relative to this script's location
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
rootDir := filepath.Dir(filepath.Dir(filename))
|
||||||
|
|
||||||
|
specPath := filepath.Join(rootDir, "openapi.yaml")
|
||||||
|
manifestPath := filepath.Join(rootDir, "endpoint-manifest.json")
|
||||||
|
|
||||||
|
current, err := extractEndpoints(specPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error extracting endpoints: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestData, err := os.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: endpoint-manifest.json not found. Copy it from the MCP repo first.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest []Endpoint
|
||||||
|
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing manifest: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestKeys := make(map[string]bool)
|
||||||
|
for _, e := range manifest {
|
||||||
|
manifestKeys[endpointKey(e)] = true
|
||||||
|
}
|
||||||
|
currentKeys := make(map[string]bool)
|
||||||
|
for _, e := range current {
|
||||||
|
currentKeys[endpointKey(e)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var added, removed []Endpoint
|
||||||
|
for _, e := range current {
|
||||||
|
if !manifestKeys[endpointKey(e)] {
|
||||||
|
added = append(added, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range manifest {
|
||||||
|
if !currentKeys[endpointKey(e)] {
|
||||||
|
removed = append(removed, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(added) == 0 && len(removed) == 0 {
|
||||||
|
fmt.Printf("No endpoint drift detected. %d endpoints match the manifest.\n", len(current))
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Endpoint drift detected!")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if len(added) > 0 {
|
||||||
|
fmt.Printf("New endpoints (%d):\n", len(added))
|
||||||
|
for _, e := range added {
|
||||||
|
fmt.Printf(" + %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(removed) > 0 {
|
||||||
|
fmt.Printf("Removed endpoints (%d):\n", len(removed))
|
||||||
|
for _, e := range removed {
|
||||||
|
fmt.Printf(" - %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Update endpoint-manifest.json from the MCP repo to resolve drift.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) EZSCALE.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
//go:build tools
|
//go:build tools
|
||||||
|
|||||||
Reference in New Issue
Block a user