From 51c743fb2b98f014a32348cdb701018d5b1756c5 Mon Sep 17 00:00:00 2001 From: Manuel Ganter Date: Tue, 11 Nov 2025 14:15:52 +0100 Subject: [PATCH] init --- .gitignore | 44 +++ Makefile | 53 ++++ README.md | 251 ++++++++++++++++ examples/complete/main.tf | 136 +++++++++ examples/main.tf | 92 ++++++ go.mod | 32 +++ go.sum | 99 +++++++ internal/client/app.go | 105 +++++++ internal/client/appinst.go | 114 ++++++++ internal/client/client.go | 81 ++++++ internal/client/models.go | 82 ++++++ internal/provider/app_data_source.go | 177 ++++++++++++ internal/provider/app_resource.go | 311 ++++++++++++++++++++ internal/provider/appinst_data_source.go | 204 +++++++++++++ internal/provider/appinst_resource.go | 351 +++++++++++++++++++++++ internal/provider/provider.go | 224 +++++++++++++++ main.go | 31 ++ 17 files changed, 2387 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 examples/complete/main.tf create mode 100644 examples/main.tf create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/client/app.go create mode 100644 internal/client/appinst.go create mode 100644 internal/client/client.go create mode 100644 internal/client/models.go create mode 100644 internal/provider/app_data_source.go create mode 100644 internal/provider/app_resource.go create mode 100644 internal/provider/appinst_data_source.go create mode 100644 internal/provider/appinst_resource.go create mode 100644 internal/provider/provider.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2805473 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b46a40 --- /dev/null +++ b/Makefile @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3b8798 --- /dev/null +++ b/README.md @@ -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 diff --git a/examples/complete/main.tf b/examples/complete/main.tf new file mode 100644 index 0000000..80701f8 --- /dev/null +++ b/examples/complete/main.tf @@ -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 +} diff --git a/examples/main.tf b/examples/main.tf new file mode 100644 index 0000000..c9b9959 --- /dev/null +++ b/examples/main.tf @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8275fcf --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1b5a0b6 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/client/app.go b/internal/client/app.go new file mode 100644 index 0000000..55128b3 --- /dev/null +++ b/internal/client/app.go @@ -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 +} diff --git a/internal/client/appinst.go b/internal/client/appinst.go new file mode 100644 index 0000000..c335c79 --- /dev/null +++ b/internal/client/appinst.go @@ -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 +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..6c425eb --- /dev/null +++ b/internal/client/client.go @@ -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 +} diff --git a/internal/client/models.go b/internal/client/models.go new file mode 100644 index 0000000..ac639a6 --- /dev/null +++ b/internal/client/models.go @@ -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"` +} diff --git a/internal/provider/app_data_source.go b/internal/provider/app_data_source.go new file mode 100644 index 0000000..e676978 --- /dev/null +++ b/internal/provider/app_data_source.go @@ -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...) +} diff --git a/internal/provider/app_resource.go b/internal/provider/app_resource.go new file mode 100644 index 0000000..cbaa286 --- /dev/null +++ b/internal/provider/app_resource.go @@ -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) +} diff --git a/internal/provider/appinst_data_source.go b/internal/provider/appinst_data_source.go new file mode 100644 index 0000000..c0f090f --- /dev/null +++ b/internal/provider/appinst_data_source.go @@ -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...) +} diff --git a/internal/provider/appinst_resource.go b/internal/provider/appinst_resource.go new file mode 100644 index 0000000..c8a08fb --- /dev/null +++ b/internal/provider/appinst_resource.go @@ -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) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 0000000..bef742b --- /dev/null +++ b/internal/provider/provider.go @@ -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, + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..10c565f --- /dev/null +++ b/main.go @@ -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()) + } +}