This commit is contained in:
Manuel Ganter 2025-11-11 14:15:52 +01:00
commit 51c743fb2b
No known key found for this signature in database
17 changed files with 2387 additions and 0 deletions

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
terraform-provider-edge-connect
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Dependency directories
vendor/
# Go workspace file
go.work
# Terraform files
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
.terraformrc
terraform.rc
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

53
Makefile Normal file
View file

@ -0,0 +1,53 @@
.PHONY: build install test clean fmt vet
# Default target
default: build
# Build the provider
build:
go build -o terraform-provider-edge-connect
# Install the provider locally
install: build
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/DevFW-CICD/edge-connect/1.0.0/$$(go env GOOS)_$$(go env GOARCH)
cp terraform-provider-edge-connect ~/.terraform.d/plugins/registry.terraform.io/DevFW-CICD/edge-connect/1.0.0/$$(go env GOOS)_$$(go env GOARCH)/
# Run tests
test:
go test -v ./...
# Clean build artifacts
clean:
rm -f terraform-provider-edge-connect
rm -rf .terraform/
rm -f .terraform.lock.hcl
rm -f terraform.tfstate*
# Format Go code
fmt:
go fmt ./...
# Run go vet
vet:
go vet ./...
# Lint the code
lint:
golangci-lint run
# Run all checks
check: fmt vet test
# Build for multiple platforms
build-all:
GOOS=darwin GOARCH=amd64 go build -o bin/terraform-provider-edge-connect_darwin_amd64
GOOS=darwin GOARCH=arm64 go build -o bin/terraform-provider-edge-connect_darwin_arm64
GOOS=linux GOARCH=amd64 go build -o bin/terraform-provider-edge-connect_linux_amd64
GOOS=linux GOARCH=arm64 go build -o bin/terraform-provider-edge-connect_linux_arm64
GOOS=windows GOARCH=amd64 go build -o bin/terraform-provider-edge-connect_windows_amd64.exe
# Generate documentation
docs:
@echo "Documentation should be generated using terraform-plugin-docs"
@echo "Run: go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@latest"
@echo "Then: tfplugindocs generate"

251
README.md Normal file
View file

@ -0,0 +1,251 @@
# Terraform Provider for Edge Connect
This Terraform provider allows you to manage Edge Connect applications and application instances.
## Features
- Manage application specifications (`edge-connect_app` resource)
- Manage application instances (`edge-connect_appinst` resource)
- Query existing applications and instances (data sources)
- Support for bearer token and basic authentication
- Full CRUD operations for all resources
## Requirements
- [Terraform](https://www.terraform.io/downloads.html) >= 1.0
- [Go](https://golang.org/doc/install) >= 1.21 (for development)
- Access to an Edge Connect API endpoint
## Installation
### Using Terraform Registry (Recommended)
```hcl
terraform {
required_providers {
edge-connect = {
source = "DevFW-CICD/edge-connect"
version = "~> 1.0"
}
}
}
```
### Local Development
1. Clone the repository:
```bash
git clone ssh://git@edp.buildth.ing/DevFW-CICD/terraform-provider-edge-connect.git
cd terraform-provider-edge-connect
```
2. Build the provider:
```bash
go build -o terraform-provider-edge-connect
```
3. Install locally:
```bash
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/DevFW-CICD/edge-connect/1.0.0/darwin_arm64
cp terraform-provider-edge-connect ~/.terraform.d/plugins/registry.terraform.io/DevFW-CICD/edge-connect/1.0.0/darwin_arm64/
```
Note: Adjust the path based on your OS and architecture (e.g., `linux_amd64`, `darwin_amd64`, etc.)
## Usage
### Provider Configuration
```hcl
provider "edge-connect" {
base_url = "https://edp.buildth.ing"
token = var.edge_connect_token
}
```
Or using basic authentication:
```hcl
provider "edge-connect" {
base_url = "https://edp.buildth.ing"
username = var.edge_connect_username
password = var.edge_connect_password
}
```
Configuration can also be provided via environment variables:
- `EDGE_CONNECT_BASE_URL`
- `EDGE_CONNECT_TOKEN`
- `EDGE_CONNECT_USERNAME`
- `EDGE_CONNECT_PASSWORD`
### Creating an Application
```hcl
resource "edge-connect_app" "example" {
region = "EU"
organization = "myorg"
name = "my-app"
version = "1.0.0"
image_type = "Docker"
image_path = "nginx:latest"
deployment = "kubernetes"
default_flavor = "EU.small"
access_ports = "tcp:80,tcp:443"
}
```
### Creating an Application Instance
```hcl
resource "edge-connect_appinst" "example" {
region = "EU"
app_organization = edge-connect_app.example.organization
app_name = edge-connect_app.example.name
app_version = edge-connect_app.example.version
cloudlet_organization = "cloudlet-org"
cloudlet_name = "edge-cloudlet-1"
cluster_organization = "cluster-org"
flavor = "EU.medium"
}
```
### Using Data Sources
```hcl
data "edge-connect_app" "existing" {
region = "EU"
organization = "myorg"
name = "existing-app"
version = "2.0.0"
}
output "app_image" {
value = data.edge-connect_app.existing.image_path
}
```
## Resources
### `edge-connect_app`
Manages an Edge Connect application specification.
#### Arguments
- `region` (Required, Forces new resource) - The region where the app is deployed (e.g., 'EU')
- `organization` (Required, Forces new resource) - The organization that owns the app
- `name` (Required, Forces new resource) - The name of the application
- `version` (Required, Forces new resource) - The version of the application
- `image_type` (Required) - The type of image (e.g., 'Docker')
- `image_path` (Required) - The path to the container image
- `deployment` (Required) - The deployment type (e.g., 'kubernetes')
- `default_flavor` (Optional) - The default flavor (e.g., 'EU.small', 'EU.medium', 'EU.big', 'EU.large')
- `deployment_manifest` (Optional) - The Kubernetes deployment manifest (YAML)
- `access_ports` (Optional) - The access ports in format 'protocol:port' (e.g., 'tcp:80,tcp:443')
- `annotations` (Optional) - Annotations for the app
#### Attributes
- `id` - The unique identifier (format: region/organization/name/version)
- `created_at` - The timestamp when the app was created
- `updated_at` - The timestamp when the app was last updated
### `edge-connect_appinst`
Manages an Edge Connect application instance.
#### Arguments
- `region` (Required, Forces new resource) - The region where the app instance is deployed
- `app_organization` (Required, Forces new resource) - The organization that owns the app
- `app_name` (Required, Forces new resource) - The name of the application
- `app_version` (Required, Forces new resource) - The version of the application
- `cloudlet_organization` (Required, Forces new resource) - The organization that owns the cloudlet
- `cloudlet_name` (Required, Forces new resource) - The name of the cloudlet
- `cluster_organization` (Required, Forces new resource) - The organization that owns the cluster
- `cloudlet` (Optional) - The cloudlet identifier
- `flavor` (Optional) - The flavor for the app instance
#### Attributes
- `id` - The unique identifier
- `real_cluster_name` - The real cluster name
- `state` - The state of the app instance
- `runtime_info` - Runtime information for the app instance
- `uri` - The URI to access the app instance
- `liveness` - The liveness status of the app instance
- `power_state` - The power state of the app instance
- `created_at` - The timestamp when the app instance was created
- `updated_at` - The timestamp when the app instance was last updated
## Data Sources
### `data.edge-connect_app`
Fetches information about an existing Edge Connect application.
#### Arguments
- `region` (Required) - The region where the app is deployed
- `organization` (Required) - The organization that owns the app
- `name` (Required) - The name of the application
- `version` (Required) - The version of the application
### `data.edge-connect_appinst`
Fetches information about an existing Edge Connect application instance.
#### Arguments
- `region` (Required) - The region where the app instance is deployed
- `app_organization` (Required) - The organization that owns the app
- `app_name` (Required) - The name of the application
- `app_version` (Required) - The version of the application
- `cloudlet_organization` (Required) - The organization that owns the cloudlet
- `cloudlet_name` (Required) - The name of the cloudlet
- `cluster_organization` (Required) - The organization that owns the cluster
## Examples
See the [examples](./examples) directory for complete usage examples.
## Development
### Building
```bash
go build -o terraform-provider-edge-connect
```
### Testing
```bash
go test ./...
```
### Running Example
```bash
cd examples
terraform init
terraform plan
terraform apply
```
## Contributing
Contributions are welcome! Please submit pull requests or open issues on the repository.
## License
This provider is distributed under the Mozilla Public License 2.0. See LICENSE for more information.
## Support
For issues and questions:
- Open an issue on the repository
- Contact the DevFW-CICD team at https://edp.buildth.ing

136
examples/complete/main.tf Normal file
View file

@ -0,0 +1,136 @@
terraform {
required_providers {
edge-connect = {
source = "DevFW-CICD/edge-connect"
}
}
}
provider "edge-connect" {
base_url = "https://edp.buildth.ing"
token = var.edge_connect_token
}
variable "edge_connect_token" {
description = "Edge Connect API token"
type = string
sensitive = true
}
# Create a web application
resource "edge-connect_app" "web_app" {
region = "EU"
organization = "acme-corp"
name = "web-frontend"
version = "1.0.0"
image_type = "Docker"
image_path = "nginx:alpine"
deployment = "kubernetes"
default_flavor = "EU.small"
access_ports = "tcp:80,tcp:443"
# Optional Kubernetes deployment manifest
deployment_manifest = <<-EOT
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-frontend
spec:
replicas: 2
selector:
matchLabels:
app: web-frontend
template:
metadata:
labels:
app: web-frontend
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: web-frontend
spec:
selector:
app: web-frontend
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
EOT
annotations = "team=platform,env=production"
}
# Create an API backend application
resource "edge-connect_app" "api_backend" {
region = "EU"
organization = "acme-corp"
name = "api-backend"
version = "2.3.1"
image_type = "Docker"
image_path = "acme/api-server:2.3.1"
deployment = "kubernetes"
default_flavor = "EU.medium"
access_ports = "tcp:8080"
annotations = "team=backend,env=production"
}
# Deploy the web app to edge cloudlet
resource "edge-connect_appinst" "web_instance" {
region = "EU"
app_organization = edge-connect_app.web_app.organization
app_name = edge-connect_app.web_app.name
app_version = edge-connect_app.web_app.version
cloudlet_organization = "edge-provider"
cloudlet_name = "eu-west-1"
cluster_organization = "acme-corp"
flavor = "EU.medium"
}
# Deploy the API backend to edge cloudlet
resource "edge-connect_appinst" "api_instance" {
region = "EU"
app_organization = edge-connect_app.api_backend.organization
app_name = edge-connect_app.api_backend.name
app_version = edge-connect_app.api_backend.version
cloudlet_organization = "edge-provider"
cloudlet_name = "eu-west-1"
cluster_organization = "acme-corp"
flavor = "EU.large"
}
# Outputs
output "web_app_uri" {
description = "URI to access the web application"
value = edge-connect_appinst.web_instance.uri
}
output "web_app_state" {
description = "Current state of the web application instance"
value = edge-connect_appinst.web_instance.state
}
output "api_backend_uri" {
description = "URI to access the API backend"
value = edge-connect_appinst.api_instance.uri
}
output "api_backend_state" {
description = "Current state of the API backend instance"
value = edge-connect_appinst.api_instance.state
}

92
examples/main.tf Normal file
View file

@ -0,0 +1,92 @@
terraform {
required_providers {
edge-connect = {
source = "DevFW-CICD/edge-connect"
}
}
}
provider "edge-connect" {
base_url = "https://edp.buildth.ing"
token = var.edge_connect_token
# Alternatively, use username and password:
# username = var.edge_connect_username
# password = var.edge_connect_password
}
variable "edge_connect_token" {
description = "Edge Connect API token"
type = string
sensitive = true
}
# Create an application specification
resource "edge-connect_app" "example" {
region = "EU"
organization = "myorg"
name = "my-app"
version = "1.0.0"
image_type = "Docker"
image_path = "nginx:latest"
deployment = "kubernetes"
default_flavor = "EU.small"
access_ports = "tcp:80,tcp:443"
annotations = "env=production"
}
# Create an application instance
resource "edge-connect_appinst" "example" {
region = "EU"
# Reference to the app
app_organization = edge-connect_app.example.organization
app_name = edge-connect_app.example.name
app_version = edge-connect_app.example.version
# Cloudlet and cluster configuration
cloudlet_organization = "cloudlet-org"
cloudlet_name = "edge-cloudlet-1"
cluster_organization = "cluster-org"
# Instance configuration
flavor = "EU.medium"
}
# Data source to read an existing app
data "edge-connect_app" "existing" {
region = "EU"
organization = "myorg"
name = "existing-app"
version = "2.0.0"
}
# Data source to read an existing app instance
data "edge-connect_appinst" "existing" {
region = "EU"
app_organization = "myorg"
app_name = "existing-app"
app_version = "2.0.0"
cloudlet_organization = "cloudlet-org"
cloudlet_name = "edge-cloudlet-1"
cluster_organization = "cluster-org"
}
# Outputs
output "app_id" {
value = edge-connect_app.example.id
}
output "app_instance_uri" {
value = edge-connect_appinst.example.uri
}
output "app_instance_state" {
value = edge-connect_appinst.example.state
}
output "existing_app_image" {
value = data.edge-connect_app.existing.image_path
}

32
go.mod Normal file
View file

@ -0,0 +1,32 @@
module github.com/DevFW-CICD/terraform-provider-edge-connect
go 1.25.3
require (
github.com/hashicorp/terraform-plugin-framework v1.16.1
github.com/hashicorp/terraform-plugin-log v0.9.0
)
require (
github.com/fatih/color v1.15.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.7.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/terraform-plugin-go v0.29.0 // indirect
github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

99
go.sum Normal file
View file

@ -0,0 +1,99 @@
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
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/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9u9DtyYHyEuhVOfeIXbteWA=
github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

105
internal/client/app.go Normal file
View file

@ -0,0 +1,105 @@
package client
import (
"encoding/json"
"fmt"
)
// CreateApp creates a new application
func (c *Client) CreateApp(region string, app App) (*App, error) {
req := AppRequest{
Region: region,
App: app,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/CreateApp", req)
if err != nil {
return nil, fmt.Errorf("failed to create app: %w", err)
}
var createdApp App
if err := json.Unmarshal(respBody, &createdApp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &createdApp, nil
}
// GetApp retrieves an application by key
func (c *Client) GetApp(region string, app App) (*App, error) {
req := ShowAppRequest{
Region: region,
App: &app,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/ShowApp", req)
if err != nil {
return nil, fmt.Errorf("failed to get app: %w", err)
}
var apps []App
if err := json.Unmarshal(respBody, &apps); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
if len(apps) == 0 {
return nil, fmt.Errorf("app not found")
}
return &apps[0], nil
}
// ListApps lists all applications
func (c *Client) ListApps(region string, filter *App) ([]App, error) {
req := ShowAppRequest{
Region: region,
App: filter,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/ShowApp", req)
if err != nil {
return nil, fmt.Errorf("failed to list apps: %w", err)
}
var apps []App
if err := json.Unmarshal(respBody, &apps); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return apps, nil
}
// UpdateApp updates an existing application
func (c *Client) UpdateApp(region string, app App) (*App, error) {
req := AppRequest{
Region: region,
App: app,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/UpdateApp", req)
if err != nil {
return nil, fmt.Errorf("failed to update app: %w", err)
}
var updatedApp App
if err := json.Unmarshal(respBody, &updatedApp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &updatedApp, nil
}
// DeleteApp deletes an application
func (c *Client) DeleteApp(region string, app App) error {
req := AppRequest{
Region: region,
App: app,
}
_, err := c.doRequest("POST", "/api/v1/auth/ctrl/DeleteApp", req)
if err != nil {
return fmt.Errorf("failed to delete app: %w", err)
}
return nil
}

114
internal/client/appinst.go Normal file
View file

@ -0,0 +1,114 @@
package client
import (
"encoding/json"
"fmt"
)
// CreateAppInst creates a new application instance
func (c *Client) CreateAppInst(region string, appInst AppInst) (*AppInst, error) {
req := AppInstRequest{
Region: region,
AppInst: appInst,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/CreateAppInst", req)
if err != nil {
return nil, fmt.Errorf("failed to create app instance: %w", err)
}
// The API returns an array of messages, but we want the final state
// For now, we'll just return the input appInst as created
// A more sophisticated implementation would parse the messages
var messages []map[string]interface{}
if err := json.Unmarshal(respBody, &messages); err != nil {
// If it's not an array of messages, try to unmarshal as AppInst
var createdAppInst AppInst
if err := json.Unmarshal(respBody, &createdAppInst); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &createdAppInst, nil
}
// Return the input appInst (creation was successful)
return &appInst, nil
}
// GetAppInst retrieves an application instance by key
func (c *Client) GetAppInst(region string, appInst AppInst) (*AppInst, error) {
req := ShowAppInstRequest{
Region: region,
AppInst: &appInst,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/ShowAppInst", req)
if err != nil {
return nil, fmt.Errorf("failed to get app instance: %w", err)
}
var appInsts []AppInst
if err := json.Unmarshal(respBody, &appInsts); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
if len(appInsts) == 0 {
return nil, fmt.Errorf("app instance not found")
}
return &appInsts[0], nil
}
// ListAppInsts lists all application instances
func (c *Client) ListAppInsts(region string, filter *AppInst) ([]AppInst, error) {
req := ShowAppInstRequest{
Region: region,
AppInst: filter,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/ShowAppInst", req)
if err != nil {
return nil, fmt.Errorf("failed to list app instances: %w", err)
}
var appInsts []AppInst
if err := json.Unmarshal(respBody, &appInsts); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return appInsts, nil
}
// UpdateAppInst updates an existing application instance
func (c *Client) UpdateAppInst(region string, appInst AppInst) (*AppInst, error) {
req := AppInstRequest{
Region: region,
AppInst: appInst,
}
respBody, err := c.doRequest("POST", "/api/v1/auth/ctrl/UpdateAppInst", req)
if err != nil {
return nil, fmt.Errorf("failed to update app instance: %w", err)
}
var updatedAppInst AppInst
if err := json.Unmarshal(respBody, &updatedAppInst); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &updatedAppInst, nil
}
// DeleteAppInst deletes an application instance
func (c *Client) DeleteAppInst(region string, appInst AppInst) error {
req := AppInstRequest{
Region: region,
AppInst: appInst,
}
_, err := c.doRequest("POST", "/api/v1/auth/ctrl/DeleteAppInst", req)
if err != nil {
return fmt.Errorf("failed to delete app instance: %w", err)
}
return nil
}

81
internal/client/client.go Normal file
View file

@ -0,0 +1,81 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client is the API client for Edge Connect
type Client struct {
BaseURL string
HTTPClient *http.Client
Token string
Username string
Password string
}
// NewClient creates a new Edge Connect API client
func NewClient(baseURL, token, username, password string) *Client {
return &Client{
BaseURL: baseURL,
HTTPClient: &http.Client{
Timeout: time.Second * 30,
},
Token: token,
Username: username,
Password: password,
}
}
// doRequest performs an HTTP request with authentication
func (c *Client) doRequest(method, path string, body interface{}) ([]byte, error) {
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewBuffer(jsonBody)
}
req, err := http.NewRequest(method, c.BaseURL+path, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
// Add authentication
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
} else if c.Username != "" && c.Password != "" {
req.SetBasicAuth(c.Username, c.Password)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
return respBody, nil
}
// HealthCheck performs a health check on the API
func (c *Client) HealthCheck() error {
_, err := c.doRequest("GET", "/api/v1/", nil)
return err
}

82
internal/client/models.go Normal file
View file

@ -0,0 +1,82 @@
package client
// App represents an application specification
type App struct {
Key struct {
Organization string `json:"organization"`
Name string `json:"name"`
Version string `json:"version"`
} `json:"key"`
Region string `json:"region,omitempty"`
ImageType string `json:"image_type,omitempty"`
ImagePath string `json:"image_path,omitempty"`
DefaultFlavor string `json:"default_flavor,omitempty"`
Deployment string `json:"deployment,omitempty"`
DeploymentManifest string `json:"deployment_manifest,omitempty"`
AccessPorts string `json:"access_ports,omitempty"`
Annotations string `json:"annotations,omitempty"`
Configs []Config `json:"configs,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
DeletePrepare bool `json:"delete_prepare,omitempty"`
}
// Config represents a configuration item
type Config struct {
Kind string `json:"kind,omitempty"`
Config string `json:"config,omitempty"`
}
// AppInst represents an application instance
type AppInst struct {
Key struct {
AppKey struct {
Organization string `json:"organization"`
Name string `json:"name"`
Version string `json:"version"`
} `json:"app_key"`
ClusterInstKey struct {
CloudletKey struct {
Organization string `json:"organization"`
Name string `json:"name"`
} `json:"cloudlet_key"`
Organization string `json:"organization"`
} `json:"cluster_inst_key"`
} `json:"key"`
Cloudlet string `json:"cloudlet,omitempty"`
Flavor string `json:"flavor,omitempty"`
RealClusterName string `json:"real_cluster_name,omitempty"`
State string `json:"state,omitempty"`
RuntimeInfo string `json:"runtime_info,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Uri string `json:"uri,omitempty"`
Liveness string `json:"liveness,omitempty"`
PowerState string `json:"power_state,omitempty"`
Configs []Config `json:"configs,omitempty"`
DeletePrepare bool `json:"delete_prepare,omitempty"`
}
// AppRequest represents a request to manage App resources
type AppRequest struct {
Region string `json:"region,omitempty"`
App App `json:"app"`
}
// AppInstRequest represents a request to manage AppInst resources
type AppInstRequest struct {
Region string `json:"region,omitempty"`
AppInst AppInst `json:"appinst"`
}
// ShowAppRequest represents a request to list/show apps
type ShowAppRequest struct {
Region string `json:"region,omitempty"`
App *App `json:"app,omitempty"`
}
// ShowAppInstRequest represents a request to list/show app instances
type ShowAppInstRequest struct {
Region string `json:"region,omitempty"`
AppInst *AppInst `json:"appinst,omitempty"`
}

View file

@ -0,0 +1,177 @@
package provider
import (
"context"
"fmt"
"github.com/DevFW-CICD/terraform-provider-edge-connect/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &appDataSource{}
_ datasource.DataSourceWithConfigure = &appDataSource{}
)
// NewAppDataSource is a helper function to simplify the provider implementation.
func NewAppDataSource() datasource.DataSource {
return &appDataSource{}
}
// appDataSource is the data source implementation.
type appDataSource struct {
client *client.Client
}
// appDataSourceModel maps the data source schema data.
type appDataSourceModel struct {
ID types.String `tfsdk:"id"`
Region types.String `tfsdk:"region"`
Organization types.String `tfsdk:"organization"`
Name types.String `tfsdk:"name"`
Version types.String `tfsdk:"version"`
ImageType types.String `tfsdk:"image_type"`
ImagePath types.String `tfsdk:"image_path"`
DefaultFlavor types.String `tfsdk:"default_flavor"`
Deployment types.String `tfsdk:"deployment"`
DeploymentManifest types.String `tfsdk:"deployment_manifest"`
AccessPorts types.String `tfsdk:"access_ports"`
Annotations types.String `tfsdk:"annotations"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// Metadata returns the data source type name.
func (d *appDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_app"
}
// Schema defines the schema for the data source.
func (d *appDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches an Edge Connect application specification.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the app (format: region/organization/name/version).",
Computed: true,
},
"region": schema.StringAttribute{
Description: "The region where the app is deployed (e.g., 'EU').",
Required: true,
},
"organization": schema.StringAttribute{
Description: "The organization that owns the app.",
Required: true,
},
"name": schema.StringAttribute{
Description: "The name of the application.",
Required: true,
},
"version": schema.StringAttribute{
Description: "The version of the application.",
Required: true,
},
"image_type": schema.StringAttribute{
Description: "The type of image (e.g., 'Docker').",
Computed: true,
},
"image_path": schema.StringAttribute{
Description: "The path to the container image.",
Computed: true,
},
"default_flavor": schema.StringAttribute{
Description: "The default flavor for the app.",
Computed: true,
},
"deployment": schema.StringAttribute{
Description: "The deployment type (e.g., 'kubernetes').",
Computed: true,
},
"deployment_manifest": schema.StringAttribute{
Description: "The Kubernetes deployment manifest (YAML).",
Computed: true,
},
"access_ports": schema.StringAttribute{
Description: "The access ports in format 'protocol:port'.",
Computed: true,
},
"annotations": schema.StringAttribute{
Description: "Annotations for the app.",
Computed: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp when the app was created.",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The timestamp when the app was last updated.",
Computed: true,
},
},
}
}
// Configure adds the provider configured client to the data source.
func (d *appDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, 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 = client
}
// Read refreshes the Terraform state with the latest data.
func (d *appDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config appDataSourceModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get app from API
app := client.App{}
app.Key.Organization = config.Organization.ValueString()
app.Key.Name = config.Name.ValueString()
app.Key.Version = config.Version.ValueString()
readApp, err := d.client.GetApp(config.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error reading app",
"Could not read app: "+err.Error(),
)
return
}
// Map response to data source model
config.ID = types.StringValue(fmt.Sprintf("%s/%s/%s/%s",
config.Region.ValueString(),
readApp.Key.Organization,
readApp.Key.Name,
readApp.Key.Version))
config.ImageType = types.StringValue(readApp.ImageType)
config.ImagePath = types.StringValue(readApp.ImagePath)
config.DefaultFlavor = types.StringValue(readApp.DefaultFlavor)
config.Deployment = types.StringValue(readApp.Deployment)
config.DeploymentManifest = types.StringValue(readApp.DeploymentManifest)
config.AccessPorts = types.StringValue(readApp.AccessPorts)
config.Annotations = types.StringValue(readApp.Annotations)
config.CreatedAt = types.StringValue(readApp.CreatedAt)
config.UpdatedAt = types.StringValue(readApp.UpdatedAt)
diags = resp.State.Set(ctx, &config)
resp.Diagnostics.Append(diags...)
}

View file

@ -0,0 +1,311 @@
package provider
import (
"context"
"fmt"
"github.com/DevFW-CICD/terraform-provider-edge-connect/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"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &appResource{}
_ resource.ResourceWithConfigure = &appResource{}
_ resource.ResourceWithImportState = &appResource{}
)
// NewAppResource is a helper function to simplify the provider implementation.
func NewAppResource() resource.Resource {
return &appResource{}
}
// appResource is the resource implementation.
type appResource struct {
client *client.Client
}
// appResourceModel maps the resource schema data.
type appResourceModel struct {
ID types.String `tfsdk:"id"`
Region types.String `tfsdk:"region"`
Organization types.String `tfsdk:"organization"`
Name types.String `tfsdk:"name"`
Version types.String `tfsdk:"version"`
ImageType types.String `tfsdk:"image_type"`
ImagePath types.String `tfsdk:"image_path"`
DefaultFlavor types.String `tfsdk:"default_flavor"`
Deployment types.String `tfsdk:"deployment"`
DeploymentManifest types.String `tfsdk:"deployment_manifest"`
AccessPorts types.String `tfsdk:"access_ports"`
Annotations types.String `tfsdk:"annotations"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// Metadata returns the resource type name.
func (r *appResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_app"
}
// Schema defines the schema for the resource.
func (r *appResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an Edge Connect application specification.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the app (format: region/organization/name/version).",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"region": schema.StringAttribute{
Description: "The region where the app is deployed (e.g., 'EU').",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"organization": schema.StringAttribute{
Description: "The organization that owns the app.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
Description: "The name of the application.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"version": schema.StringAttribute{
Description: "The version of the application.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"image_type": schema.StringAttribute{
Description: "The type of image (e.g., 'Docker').",
Required: true,
},
"image_path": schema.StringAttribute{
Description: "The path to the container image.",
Required: true,
},
"default_flavor": schema.StringAttribute{
Description: "The default flavor for the app (e.g., 'EU.small', 'EU.medium', 'EU.big', 'EU.large').",
Optional: true,
},
"deployment": schema.StringAttribute{
Description: "The deployment type (e.g., 'kubernetes').",
Required: true,
},
"deployment_manifest": schema.StringAttribute{
Description: "The Kubernetes deployment manifest (YAML).",
Optional: true,
},
"access_ports": schema.StringAttribute{
Description: "The access ports in format 'protocol:port' (e.g., 'tcp:80,tcp:443').",
Optional: true,
},
"annotations": schema.StringAttribute{
Description: "Annotations for the app.",
Optional: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp when the app was created.",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The timestamp when the app was last updated.",
Computed: true,
},
},
}
}
// Configure adds the provider configured client to the resource.
func (r *appResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, 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 = client
}
// Create creates the resource and sets the initial Terraform state.
func (r *appResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan appResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create new app
app := client.App{
Region: plan.Region.ValueString(),
ImageType: plan.ImageType.ValueString(),
ImagePath: plan.ImagePath.ValueString(),
DefaultFlavor: plan.DefaultFlavor.ValueString(),
Deployment: plan.Deployment.ValueString(),
DeploymentManifest: plan.DeploymentManifest.ValueString(),
AccessPorts: plan.AccessPorts.ValueString(),
Annotations: plan.Annotations.ValueString(),
}
app.Key.Organization = plan.Organization.ValueString()
app.Key.Name = plan.Name.ValueString()
app.Key.Version = plan.Version.ValueString()
createdApp, err := r.client.CreateApp(plan.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error creating app",
"Could not create app: "+err.Error(),
)
return
}
// Map response body to schema and populate Computed attribute values
plan.ID = types.StringValue(fmt.Sprintf("%s/%s/%s/%s",
plan.Region.ValueString(),
createdApp.Key.Organization,
createdApp.Key.Name,
createdApp.Key.Version))
plan.CreatedAt = types.StringValue(createdApp.CreatedAt)
plan.UpdatedAt = types.StringValue(createdApp.UpdatedAt)
tflog.Trace(ctx, "created app resource")
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
// Read refreshes the Terraform state with the latest data.
func (r *appResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state appResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get app from API
app := client.App{}
app.Key.Organization = state.Organization.ValueString()
app.Key.Name = state.Name.ValueString()
app.Key.Version = state.Version.ValueString()
readApp, err := r.client.GetApp(state.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error reading app",
"Could not read app: "+err.Error(),
)
return
}
// Map response to state
state.ImageType = types.StringValue(readApp.ImageType)
state.ImagePath = types.StringValue(readApp.ImagePath)
state.DefaultFlavor = types.StringValue(readApp.DefaultFlavor)
state.Deployment = types.StringValue(readApp.Deployment)
state.DeploymentManifest = types.StringValue(readApp.DeploymentManifest)
state.AccessPorts = types.StringValue(readApp.AccessPorts)
state.Annotations = types.StringValue(readApp.Annotations)
state.CreatedAt = types.StringValue(readApp.CreatedAt)
state.UpdatedAt = types.StringValue(readApp.UpdatedAt)
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *appResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan appResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Update app
app := client.App{
Region: plan.Region.ValueString(),
ImageType: plan.ImageType.ValueString(),
ImagePath: plan.ImagePath.ValueString(),
DefaultFlavor: plan.DefaultFlavor.ValueString(),
Deployment: plan.Deployment.ValueString(),
DeploymentManifest: plan.DeploymentManifest.ValueString(),
AccessPorts: plan.AccessPorts.ValueString(),
Annotations: plan.Annotations.ValueString(),
}
app.Key.Organization = plan.Organization.ValueString()
app.Key.Name = plan.Name.ValueString()
app.Key.Version = plan.Version.ValueString()
updatedApp, err := r.client.UpdateApp(plan.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error updating app",
"Could not update app: "+err.Error(),
)
return
}
// Update computed attributes
plan.UpdatedAt = types.StringValue(updatedApp.UpdatedAt)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *appResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state appResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Delete app
app := client.App{}
app.Key.Organization = state.Organization.ValueString()
app.Key.Name = state.Name.ValueString()
app.Key.Version = state.Version.ValueString()
err := r.client.DeleteApp(state.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error deleting app",
"Could not delete app: "+err.Error(),
)
return
}
}
// ImportState imports the resource state.
func (r *appResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import format: region/organization/name/version
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View file

@ -0,0 +1,204 @@
package provider
import (
"context"
"fmt"
"github.com/DevFW-CICD/terraform-provider-edge-connect/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &appInstDataSource{}
_ datasource.DataSourceWithConfigure = &appInstDataSource{}
)
// NewAppInstDataSource is a helper function to simplify the provider implementation.
func NewAppInstDataSource() datasource.DataSource {
return &appInstDataSource{}
}
// appInstDataSource is the data source implementation.
type appInstDataSource struct {
client *client.Client
}
// appInstDataSourceModel maps the data source schema data.
type appInstDataSourceModel struct {
ID types.String `tfsdk:"id"`
Region types.String `tfsdk:"region"`
AppOrganization types.String `tfsdk:"app_organization"`
AppName types.String `tfsdk:"app_name"`
AppVersion types.String `tfsdk:"app_version"`
CloudletOrganization types.String `tfsdk:"cloudlet_organization"`
CloudletName types.String `tfsdk:"cloudlet_name"`
ClusterOrganization types.String `tfsdk:"cluster_organization"`
Cloudlet types.String `tfsdk:"cloudlet"`
Flavor types.String `tfsdk:"flavor"`
RealClusterName types.String `tfsdk:"real_cluster_name"`
State types.String `tfsdk:"state"`
RuntimeInfo types.String `tfsdk:"runtime_info"`
Uri types.String `tfsdk:"uri"`
Liveness types.String `tfsdk:"liveness"`
PowerState types.String `tfsdk:"power_state"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// Metadata returns the data source type name.
func (d *appInstDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_appinst"
}
// Schema defines the schema for the data source.
func (d *appInstDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches an Edge Connect application instance.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the app instance.",
Computed: true,
},
"region": schema.StringAttribute{
Description: "The region where the app instance is deployed (e.g., 'EU').",
Required: true,
},
"app_organization": schema.StringAttribute{
Description: "The organization that owns the app.",
Required: true,
},
"app_name": schema.StringAttribute{
Description: "The name of the application.",
Required: true,
},
"app_version": schema.StringAttribute{
Description: "The version of the application.",
Required: true,
},
"cloudlet_organization": schema.StringAttribute{
Description: "The organization that owns the cloudlet.",
Required: true,
},
"cloudlet_name": schema.StringAttribute{
Description: "The name of the cloudlet.",
Required: true,
},
"cluster_organization": schema.StringAttribute{
Description: "The organization that owns the cluster.",
Required: true,
},
"cloudlet": schema.StringAttribute{
Description: "The cloudlet identifier.",
Computed: true,
},
"flavor": schema.StringAttribute{
Description: "The flavor for the app instance.",
Computed: true,
},
"real_cluster_name": schema.StringAttribute{
Description: "The real cluster name.",
Computed: true,
},
"state": schema.StringAttribute{
Description: "The state of the app instance.",
Computed: true,
},
"runtime_info": schema.StringAttribute{
Description: "Runtime information for the app instance.",
Computed: true,
},
"uri": schema.StringAttribute{
Description: "The URI to access the app instance.",
Computed: true,
},
"liveness": schema.StringAttribute{
Description: "The liveness status of the app instance.",
Computed: true,
},
"power_state": schema.StringAttribute{
Description: "The power state of the app instance.",
Computed: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp when the app instance was created.",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The timestamp when the app instance was last updated.",
Computed: true,
},
},
}
}
// Configure adds the provider configured client to the data source.
func (d *appInstDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, 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 = client
}
// Read refreshes the Terraform state with the latest data.
func (d *appInstDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config appInstDataSourceModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get app instance from API
appInst := client.AppInst{}
appInst.Key.AppKey.Organization = config.AppOrganization.ValueString()
appInst.Key.AppKey.Name = config.AppName.ValueString()
appInst.Key.AppKey.Version = config.AppVersion.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Organization = config.CloudletOrganization.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Name = config.CloudletName.ValueString()
appInst.Key.ClusterInstKey.Organization = config.ClusterOrganization.ValueString()
readAppInst, err := d.client.GetAppInst(config.Region.ValueString(), appInst)
if err != nil {
resp.Diagnostics.AddError(
"Error reading app instance",
"Could not read app instance: "+err.Error(),
)
return
}
// Map response to data source model
config.ID = types.StringValue(fmt.Sprintf("%s/%s/%s/%s/%s/%s/%s",
config.Region.ValueString(),
config.AppOrganization.ValueString(),
config.AppName.ValueString(),
config.AppVersion.ValueString(),
config.CloudletOrganization.ValueString(),
config.CloudletName.ValueString(),
config.ClusterOrganization.ValueString()))
config.Cloudlet = types.StringValue(readAppInst.Cloudlet)
config.Flavor = types.StringValue(readAppInst.Flavor)
config.RealClusterName = types.StringValue(readAppInst.RealClusterName)
config.State = types.StringValue(readAppInst.State)
config.RuntimeInfo = types.StringValue(readAppInst.RuntimeInfo)
config.Uri = types.StringValue(readAppInst.Uri)
config.Liveness = types.StringValue(readAppInst.Liveness)
config.PowerState = types.StringValue(readAppInst.PowerState)
config.CreatedAt = types.StringValue(readAppInst.CreatedAt)
config.UpdatedAt = types.StringValue(readAppInst.UpdatedAt)
diags = resp.State.Set(ctx, &config)
resp.Diagnostics.Append(diags...)
}

View file

@ -0,0 +1,351 @@
package provider
import (
"context"
"fmt"
"github.com/DevFW-CICD/terraform-provider-edge-connect/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"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &appInstResource{}
_ resource.ResourceWithConfigure = &appInstResource{}
_ resource.ResourceWithImportState = &appInstResource{}
)
// NewAppInstResource is a helper function to simplify the provider implementation.
func NewAppInstResource() resource.Resource {
return &appInstResource{}
}
// appInstResource is the resource implementation.
type appInstResource struct {
client *client.Client
}
// appInstResourceModel maps the resource schema data.
type appInstResourceModel struct {
ID types.String `tfsdk:"id"`
Region types.String `tfsdk:"region"`
AppOrganization types.String `tfsdk:"app_organization"`
AppName types.String `tfsdk:"app_name"`
AppVersion types.String `tfsdk:"app_version"`
CloudletOrganization types.String `tfsdk:"cloudlet_organization"`
CloudletName types.String `tfsdk:"cloudlet_name"`
ClusterOrganization types.String `tfsdk:"cluster_organization"`
Cloudlet types.String `tfsdk:"cloudlet"`
Flavor types.String `tfsdk:"flavor"`
RealClusterName types.String `tfsdk:"real_cluster_name"`
State types.String `tfsdk:"state"`
RuntimeInfo types.String `tfsdk:"runtime_info"`
Uri types.String `tfsdk:"uri"`
Liveness types.String `tfsdk:"liveness"`
PowerState types.String `tfsdk:"power_state"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// Metadata returns the resource type name.
func (r *appInstResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_appinst"
}
// Schema defines the schema for the resource.
func (r *appInstResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an Edge Connect application instance.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the app instance.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"region": schema.StringAttribute{
Description: "The region where the app instance is deployed (e.g., 'EU').",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"app_organization": schema.StringAttribute{
Description: "The organization that owns the app.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"app_name": schema.StringAttribute{
Description: "The name of the application.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"app_version": schema.StringAttribute{
Description: "The version of the application.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"cloudlet_organization": schema.StringAttribute{
Description: "The organization that owns the cloudlet.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"cloudlet_name": schema.StringAttribute{
Description: "The name of the cloudlet.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"cluster_organization": schema.StringAttribute{
Description: "The organization that owns the cluster.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"cloudlet": schema.StringAttribute{
Description: "The cloudlet identifier.",
Optional: true,
},
"flavor": schema.StringAttribute{
Description: "The flavor for the app instance (e.g., 'EU.small', 'EU.medium', 'EU.big', 'EU.large').",
Optional: true,
},
"real_cluster_name": schema.StringAttribute{
Description: "The real cluster name.",
Computed: true,
},
"state": schema.StringAttribute{
Description: "The state of the app instance.",
Computed: true,
},
"runtime_info": schema.StringAttribute{
Description: "Runtime information for the app instance.",
Computed: true,
},
"uri": schema.StringAttribute{
Description: "The URI to access the app instance.",
Computed: true,
},
"liveness": schema.StringAttribute{
Description: "The liveness status of the app instance.",
Computed: true,
},
"power_state": schema.StringAttribute{
Description: "The power state of the app instance.",
Computed: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp when the app instance was created.",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The timestamp when the app instance was last updated.",
Computed: true,
},
},
}
}
// Configure adds the provider configured client to the resource.
func (r *appInstResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, 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 = client
}
// Create creates the resource and sets the initial Terraform state.
func (r *appInstResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan appInstResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create new app instance
appInst := client.AppInst{
Cloudlet: plan.Cloudlet.ValueString(),
Flavor: plan.Flavor.ValueString(),
}
appInst.Key.AppKey.Organization = plan.AppOrganization.ValueString()
appInst.Key.AppKey.Name = plan.AppName.ValueString()
appInst.Key.AppKey.Version = plan.AppVersion.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Organization = plan.CloudletOrganization.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Name = plan.CloudletName.ValueString()
appInst.Key.ClusterInstKey.Organization = plan.ClusterOrganization.ValueString()
createdAppInst, err := r.client.CreateAppInst(plan.Region.ValueString(), appInst)
if err != nil {
resp.Diagnostics.AddError(
"Error creating app instance",
"Could not create app instance: "+err.Error(),
)
return
}
// Map response body to schema and populate Computed attribute values
plan.ID = types.StringValue(fmt.Sprintf("%s/%s/%s/%s/%s/%s/%s",
plan.Region.ValueString(),
plan.AppOrganization.ValueString(),
plan.AppName.ValueString(),
plan.AppVersion.ValueString(),
plan.CloudletOrganization.ValueString(),
plan.CloudletName.ValueString(),
plan.ClusterOrganization.ValueString()))
plan.RealClusterName = types.StringValue(createdAppInst.RealClusterName)
plan.State = types.StringValue(createdAppInst.State)
plan.RuntimeInfo = types.StringValue(createdAppInst.RuntimeInfo)
plan.Uri = types.StringValue(createdAppInst.Uri)
plan.Liveness = types.StringValue(createdAppInst.Liveness)
plan.PowerState = types.StringValue(createdAppInst.PowerState)
plan.CreatedAt = types.StringValue(createdAppInst.CreatedAt)
plan.UpdatedAt = types.StringValue(createdAppInst.UpdatedAt)
tflog.Trace(ctx, "created app instance resource")
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
// Read refreshes the Terraform state with the latest data.
func (r *appInstResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state appInstResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get app instance from API
appInst := client.AppInst{}
appInst.Key.AppKey.Organization = state.AppOrganization.ValueString()
appInst.Key.AppKey.Name = state.AppName.ValueString()
appInst.Key.AppKey.Version = state.AppVersion.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Organization = state.CloudletOrganization.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Name = state.CloudletName.ValueString()
appInst.Key.ClusterInstKey.Organization = state.ClusterOrganization.ValueString()
readAppInst, err := r.client.GetAppInst(state.Region.ValueString(), appInst)
if err != nil {
resp.Diagnostics.AddError(
"Error reading app instance",
"Could not read app instance: "+err.Error(),
)
return
}
// Map response to state
state.Cloudlet = types.StringValue(readAppInst.Cloudlet)
state.Flavor = types.StringValue(readAppInst.Flavor)
state.RealClusterName = types.StringValue(readAppInst.RealClusterName)
state.State = types.StringValue(readAppInst.State)
state.RuntimeInfo = types.StringValue(readAppInst.RuntimeInfo)
state.Uri = types.StringValue(readAppInst.Uri)
state.Liveness = types.StringValue(readAppInst.Liveness)
state.PowerState = types.StringValue(readAppInst.PowerState)
state.CreatedAt = types.StringValue(readAppInst.CreatedAt)
state.UpdatedAt = types.StringValue(readAppInst.UpdatedAt)
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *appInstResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan appInstResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Update app instance (only limited fields can be updated)
appInst := client.AppInst{
Cloudlet: plan.Cloudlet.ValueString(),
Flavor: plan.Flavor.ValueString(),
}
appInst.Key.AppKey.Organization = plan.AppOrganization.ValueString()
appInst.Key.AppKey.Name = plan.AppName.ValueString()
appInst.Key.AppKey.Version = plan.AppVersion.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Organization = plan.CloudletOrganization.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Name = plan.CloudletName.ValueString()
appInst.Key.ClusterInstKey.Organization = plan.ClusterOrganization.ValueString()
updatedAppInst, err := r.client.UpdateAppInst(plan.Region.ValueString(), appInst)
if err != nil {
resp.Diagnostics.AddError(
"Error updating app instance",
"Could not update app instance: "+err.Error(),
)
return
}
// Update computed attributes
plan.State = types.StringValue(updatedAppInst.State)
plan.UpdatedAt = types.StringValue(updatedAppInst.UpdatedAt)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *appInstResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state appInstResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Delete app instance
appInst := client.AppInst{}
appInst.Key.AppKey.Organization = state.AppOrganization.ValueString()
appInst.Key.AppKey.Name = state.AppName.ValueString()
appInst.Key.AppKey.Version = state.AppVersion.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Organization = state.CloudletOrganization.ValueString()
appInst.Key.ClusterInstKey.CloudletKey.Name = state.CloudletName.ValueString()
appInst.Key.ClusterInstKey.Organization = state.ClusterOrganization.ValueString()
err := r.client.DeleteAppInst(state.Region.ValueString(), appInst)
if err != nil {
resp.Diagnostics.AddError(
"Error deleting app instance",
"Could not delete app instance: "+err.Error(),
)
return
}
}
// ImportState imports the resource state.
func (r *appInstResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import format: region/app_org/app_name/app_version/cloudlet_org/cloudlet_name/cluster_org
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View file

@ -0,0 +1,224 @@
package provider
import (
"context"
"os"
"github.com/DevFW-CICD/terraform-provider-edge-connect/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ provider.Provider = &edgeConnectProvider{}
)
// New is a helper function to simplify provider server and testing implementation.
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &edgeConnectProvider{
version: version,
}
}
}
// edgeConnectProvider is the provider implementation.
type edgeConnectProvider struct {
version string
}
// edgeConnectProviderModel maps provider schema data to a Go type.
type edgeConnectProviderModel struct {
BaseURL types.String `tfsdk:"base_url"`
Token types.String `tfsdk:"token"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
}
// Metadata returns the provider type name.
func (p *edgeConnectProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "edge-connect"
resp.Version = p.version
}
// Schema defines the provider-level schema for configuration data.
func (p *edgeConnectProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Interact with Edge Connect API for managing applications and application instances.",
Attributes: map[string]schema.Attribute{
"base_url": schema.StringAttribute{
Description: "The base URL for the Edge Connect API. May also be provided via EDGE_CONNECT_BASE_URL environment variable.",
Optional: true,
},
"token": schema.StringAttribute{
Description: "Bearer token for authentication. May also be provided via EDGE_CONNECT_TOKEN environment variable.",
Optional: true,
Sensitive: true,
},
"username": schema.StringAttribute{
Description: "Username for basic authentication. May also be provided via EDGE_CONNECT_USERNAME environment variable.",
Optional: true,
},
"password": schema.StringAttribute{
Description: "Password for basic authentication. May also be provided via EDGE_CONNECT_PASSWORD environment variable.",
Optional: true,
Sensitive: true,
},
},
}
}
// Configure prepares a Edge Connect API client for data sources and resources.
func (p *edgeConnectProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
tflog.Info(ctx, "Configuring Edge Connect client")
// Retrieve provider data from configuration
var config edgeConnectProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// If practitioner provided a configuration value for any of the
// attributes, it must be a known value.
if config.BaseURL.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("base_url"),
"Unknown Edge Connect API Base URL",
"The provider cannot create the Edge Connect API client as there is an unknown configuration value for the base URL. "+
"Either target apply the source of the value first, set the value statically in the configuration, or use the EDGE_CONNECT_BASE_URL environment variable.",
)
}
if config.Token.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("token"),
"Unknown Edge Connect API Token",
"The provider cannot create the Edge Connect API client as there is an unknown configuration value for the token. "+
"Either target apply the source of the value first, set the value statically in the configuration, or use the EDGE_CONNECT_TOKEN environment variable.",
)
}
if config.Username.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("username"),
"Unknown Edge Connect API Username",
"The provider cannot create the Edge Connect API client as there is an unknown configuration value for the username. "+
"Either target apply the source of the value first, set the value statically in the configuration, or use the EDGE_CONNECT_USERNAME environment variable.",
)
}
if config.Password.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("password"),
"Unknown Edge Connect API Password",
"The provider cannot create the Edge Connect API client as there is an unknown configuration value for the password. "+
"Either target apply the source of the value first, set the value statically in the configuration, or use the EDGE_CONNECT_PASSWORD environment variable.",
)
}
if resp.Diagnostics.HasError() {
return
}
// Default values to environment variables, but override
// with Terraform configuration value if set.
baseURL := os.Getenv("EDGE_CONNECT_BASE_URL")
token := os.Getenv("EDGE_CONNECT_TOKEN")
username := os.Getenv("EDGE_CONNECT_USERNAME")
password := os.Getenv("EDGE_CONNECT_PASSWORD")
if !config.BaseURL.IsNull() {
baseURL = config.BaseURL.ValueString()
}
if !config.Token.IsNull() {
token = config.Token.ValueString()
}
if !config.Username.IsNull() {
username = config.Username.ValueString()
}
if !config.Password.IsNull() {
password = config.Password.ValueString()
}
// If any of the expected configurations are missing, return
// errors with provider-specific guidance.
if baseURL == "" {
resp.Diagnostics.AddAttributeError(
path.Root("base_url"),
"Missing Edge Connect API Base URL",
"The provider requires a base URL for the Edge Connect API. "+
"Set the base_url value in the configuration or use the EDGE_CONNECT_BASE_URL environment variable. "+
"If either is already set, ensure the value is not empty.",
)
}
if token == "" && (username == "" || password == "") {
resp.Diagnostics.AddError(
"Missing Edge Connect API Authentication",
"The provider requires either a bearer token or username/password for authentication. "+
"Set the token value in the configuration or use the EDGE_CONNECT_TOKEN environment variable, "+
"or set username and password values in the configuration or use the EDGE_CONNECT_USERNAME and EDGE_CONNECT_PASSWORD environment variables.",
)
}
if resp.Diagnostics.HasError() {
return
}
ctx = tflog.SetField(ctx, "edge_connect_base_url", baseURL)
ctx = tflog.SetField(ctx, "edge_connect_token", token)
ctx = tflog.SetField(ctx, "edge_connect_username", username)
ctx = tflog.SetField(ctx, "edge_connect_password", password)
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "edge_connect_token")
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "edge_connect_password")
tflog.Debug(ctx, "Creating Edge Connect client")
// Create a new Edge Connect client using the configuration values
apiClient := client.NewClient(baseURL, token, username, password)
// Test the connection
if err := apiClient.HealthCheck(); err != nil {
resp.Diagnostics.AddError(
"Unable to Connect to Edge Connect API",
"An error occurred while connecting to the Edge Connect API. "+
"Please verify that your base URL and authentication credentials are correct.\n\n"+
"Edge Connect Client Error: "+err.Error(),
)
return
}
// Make the Edge Connect client available during DataSource and Resource
// type Configure methods.
resp.DataSourceData = apiClient
resp.ResourceData = apiClient
tflog.Info(ctx, "Configured Edge Connect client", map[string]any{"success": true})
}
// DataSources defines the data sources implemented in the provider.
func (p *edgeConnectProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewAppDataSource,
NewAppInstDataSource,
}
}
// Resources defines the resources implemented in the provider.
func (p *edgeConnectProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewAppResource,
NewAppInstResource,
}
}

31
main.go Normal file
View file

@ -0,0 +1,31 @@
package main
import (
"context"
"flag"
"log"
"github.com/DevFW-CICD/terraform-provider-edge-connect/internal/provider"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
)
// version can be set at build time using -ldflags
var version string = "dev"
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/DevFW-CICD/edge-connect",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}