Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ demo
.copilot-tracking/

.codeql-results

# Go Cache
.gocache/
4 changes: 4 additions & 0 deletions build/configs/ucp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ logging:
level: "debug"
json: true

# Terraform cache path - relative to project root where debug commands are run
terraform:
path: "./debug_files/terraform-cache"

tracerProvider:
enabled: false
serviceName: "ucp"
Expand Down
10 changes: 9 additions & 1 deletion cmd/applications-rp/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"

"github.com/go-chi/chi/v5"
"github.com/go-logr/logr"
"github.com/spf13/cobra"
runtimelog "sigs.k8s.io/controller-runtime/pkg/log"
Expand All @@ -31,6 +32,7 @@ import (
"github.com/radius-project/radius/pkg/components/trace/traceservice"
"github.com/radius-project/radius/pkg/recipes/controllerconfig"
"github.com/radius-project/radius/pkg/server"
tfinstaller "github.com/radius-project/radius/pkg/terraform/installer"

"github.com/radius-project/radius/pkg/components/hosting"
"github.com/radius-project/radius/pkg/ucp/ucplog"
Expand Down Expand Up @@ -81,10 +83,16 @@ var rootCmd = &cobra.Command{
return err
}

// Create route configurer for terraform installer API endpoints
terraformRoutes := func(ctx context.Context, r chi.Router, opts hostoptions.HostOptions) error {
return tfinstaller.RegisterRoutesWithHostOptions(ctx, r, opts, opts.Config.Server.PathBase)
}

services = append(
services,
server.NewAPIService(options, builders),
server.NewAPIServiceWithRoutes(options, builders, terraformRoutes),
server.NewAsyncWorker(options, builders),
tfinstaller.NewHostOptionsWorkerService(options),
)

host := &hosting.Host{
Expand Down
20 changes: 20 additions & 0 deletions cmd/rad/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ import (
"github.com/radius-project/radius/pkg/cli/cmd/rollback"
rollback_kubernetes "github.com/radius-project/radius/pkg/cli/cmd/rollback/kubernetes"
"github.com/radius-project/radius/pkg/cli/cmd/run"
"github.com/radius-project/radius/pkg/cli/cmd/terraform"
terraform_install "github.com/radius-project/radius/pkg/cli/cmd/terraform/install"
terraform_list "github.com/radius-project/radius/pkg/cli/cmd/terraform/list"
terraform_status "github.com/radius-project/radius/pkg/cli/cmd/terraform/status"
terraform_uninstall "github.com/radius-project/radius/pkg/cli/cmd/terraform/uninstall"
"github.com/radius-project/radius/pkg/cli/cmd/uninstall"
uninstall_kubernetes "github.com/radius-project/radius/pkg/cli/cmd/uninstall/kubernetes"
"github.com/radius-project/radius/pkg/cli/cmd/upgrade"
Expand Down Expand Up @@ -452,6 +457,21 @@ func initSubCommands() {

versionCmd, _ := version.NewCommand(framework)
RootCmd.AddCommand(versionCmd)

terraformCmd := terraform.NewCommand()
RootCmd.AddCommand(terraformCmd)

terraformInstallCmd, _ := terraform_install.NewCommand(framework)
terraformCmd.AddCommand(terraformInstallCmd)

terraformUninstallCmd, _ := terraform_uninstall.NewCommand(framework)
terraformCmd.AddCommand(terraformUninstallCmd)

terraformStatusCmd, _ := terraform_status.NewCommand(framework)
terraformCmd.AddCommand(terraformStatusCmd)

terraformListCmd, _ := terraform_list.NewCommand(framework)
terraformCmd.AddCommand(terraformListCmd)
}

// The dance we do with config is kinda complex. We want commands to be able to retrieve a config (*viper.Viper)
Expand Down
124 changes: 124 additions & 0 deletions deploy/Chart/tests/terraform_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
suite: test terraform configuration
templates:
- rp/deployment.yaml
- rp/configmaps.yaml
- dynamic-rp/deployment.yaml
tests:
# applications-rp terraform volume tests
- it: should create emptyDir terraform volume in applications-rp when terraform is enabled
set:
global.terraform.enabled: true
rp.image: applications-rp
rp.tag: latest
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: terraform
emptyDir: {}
template: rp/deployment.yaml

- it: should mount terraform volume in applications-rp container
set:
global.terraform.enabled: true
rp.image: applications-rp
rp.tag: latest
rp.terraform.path: /terraform
asserts:
- contains:
path: spec.template.spec.containers[0].volumeMounts
content:
name: terraform
mountPath: /terraform
template: rp/deployment.yaml

- it: should include terraform init container when terraform is enabled
set:
global.terraform.enabled: true
rp.image: applications-rp
rp.tag: latest
asserts:
- isNotEmpty:
path: spec.template.spec.initContainers
template: rp/deployment.yaml
- contains:
path: spec.template.spec.initContainers
content:
name: terraform-init
any: true
template: rp/deployment.yaml

- it: should include terraform config in applications-rp configmap
asserts:
- matchRegex:
path: data["radius-self-host.yaml"]
pattern: "terraform:\\s+path: \"/terraform\""
template: rp/configmaps.yaml

# dynamic-rp terraform volume tests
- it: should create emptyDir terraform volume in dynamic-rp when terraform is enabled
set:
global.terraform.enabled: true
dynamicrp.image: dynamic-rp
dynamicrp.tag: latest
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: terraform
emptyDir: {}
template: dynamic-rp/deployment.yaml

- it: should mount terraform volume in dynamic-rp container
set:
global.terraform.enabled: true
dynamicrp.image: dynamic-rp
dynamicrp.tag: latest
dynamicrp.terraform.path: /terraform
asserts:
- contains:
path: spec.template.spec.containers[0].volumeMounts
content:
name: terraform
mountPath: /terraform
template: dynamic-rp/deployment.yaml

- it: should include terraform init container in dynamic-rp when terraform is enabled
set:
global.terraform.enabled: true
dynamicrp.image: dynamic-rp
dynamicrp.tag: latest
asserts:
- isNotEmpty:
path: spec.template.spec.initContainers
template: dynamic-rp/deployment.yaml
- contains:
path: spec.template.spec.initContainers
content:
name: terraform-init
any: true
template: dynamic-rp/deployment.yaml

# Both deployments use independent emptyDir volumes (pod-local storage)
- it: should use independent emptyDir volumes for each deployment
set:
global.terraform.enabled: true
rp.image: applications-rp
rp.tag: latest
dynamicrp.image: dynamic-rp
dynamicrp.tag: latest
asserts:
# applications-rp uses emptyDir
- contains:
path: spec.template.spec.volumes
content:
name: terraform
emptyDir: {}
template: rp/deployment.yaml
# dynamic-rp uses emptyDir
- contains:
path: spec.template.spec.volumes
content:
name: terraform
emptyDir: {}
template: dynamic-rp/deployment.yaml
7 changes: 7 additions & 0 deletions deploy/Chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ global:
# Valid values: TRACE, DEBUG, INFO, WARN, ERROR, OFF
# Default: ERROR
loglevel: "ERROR"
# Storage size for shared terraform PVC (used by UCP installer and recipe execution)
# This PVC stores installed Terraform versions managed via `rad terraform install`
storageSize: "1Gi"
# Storage class name for the terraform PVC
# Leave empty to use the default storage class
# For ReadWriteMany access, use a storage class that supports it (e.g., NFS, EFS, Azure Files)
storageClassName: ""

controller:
image: controller
Expand Down
23 changes: 12 additions & 11 deletions docs/ucp/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

This folder contains documentation for the Universal Control Plane (UCP).

| Topic | Description |
|-------|-------------|
|**[Overview](overview.md)** | What is UCP and why is it needed?
|**[UCP Resources](resources.md)** | List of UCP resources
|**[Addressing Scheme](addressing_scheme.md)** | Learn about UCP addresses
|**[UCP Config](configuration.md)** | Learn about UCP configuration
|**[Call Flows](call_flows.md)** | Learn the different call flows with UCP for common deployment scenarios
|**[AWS Support](aws.md)** | Details of AWS Support in UCP
|**[Developer Guide](developer_guide.md)** | Developer Guide
|**[Code Walkthrough](code_walkthrough.md)** | A broad overview of the code.
|**[References](references.md)** | References for further reading
| Topic | Description |
| ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| **[Overview](overview.md)** | What is UCP and why is it needed? |
| **[UCP Resources](resources.md)** | List of UCP resources |
| **[Addressing Scheme](addressing_scheme.md)** | Learn about UCP addresses |
| **[UCP Config](configuration.md)** | Learn about UCP configuration |
| **[Call Flows](call_flows.md)** | Learn the different call flows with UCP for common deployment scenarios |
| **[AWS Support](aws.md)** | Details of AWS Support in UCP |
| **[Developer Guide](developer_guide.md)** | Developer Guide |
| **[Code Walkthrough](code_walkthrough.md)** | A broad overview of the code. |
| **[References](references.md)** | References for further reading |
| **[Terraform Installer](terraform/terraform-installer.md)** | API for installing/uninstalling Terraform binaries |
115 changes: 115 additions & 0 deletions docs/ucp/terraform/terraform-installer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Terraform Installer API (Radius)

## Endpoints

| Method | Path | Description |
| ------ | -------------------------------- | ----------------------------- |
| `POST` | `/installer/terraform/install` | Install a Terraform version |
| `POST` | `/installer/terraform/uninstall` | Uninstall a Terraform version |
| `GET` | `/installer/terraform/status` | Get installer status |

## Install Request

Provide **either** `version` or `sourceUrl` (or both):

```json
{
"version": "1.6.4",
"sourceUrl": "https://example.com/terraform.zip",
"checksum": "sha256:abc123...",
"caBundle": "<PEM-encoded CA cert>",
"authHeader": "Bearer <token>",
"clientCert": "<PEM-encoded client cert>",
"clientKey": "<PEM-encoded client key>",
"proxyUrl": "http://proxy:8080"
}
```

| Field | Required | Description |
| ------------ | ------------------------ | ------------------------------------------------------------------------- |
| `version` | One of version/sourceUrl | Semver version (e.g., `1.6.4`, `1.6.4-beta.1`) |
| `sourceUrl` | One of version/sourceUrl | Direct download URL for Terraform archive |
| `checksum` | Recommended | SHA256 checksum (`sha256:<hex>` or bare hex) |
| `caBundle` | No | PEM-encoded CA cert for self-signed TLS (requires `sourceUrl`) |
| `authHeader` | No | Authorization header for private registries (requires `sourceUrl`) |
| `clientCert` | No | PEM-encoded client cert for mTLS (requires `sourceUrl` and `clientKey`) |
| `clientKey` | No | PEM-encoded client private key for mTLS (requires `sourceUrl` and `clientCert`) |
| `proxyUrl` | No | HTTP/HTTPS proxy URL (requires `sourceUrl`) |

**Notes:**

- If only `sourceUrl` is provided (no version), a version identifier is auto-generated from the URL hash (e.g., `custom-a1b2c3d4`)
- Bare hex checksums are also accepted (without `sha256:` prefix)
- Idempotent: re-installing an existing version promotes it to current without re-downloading

**Private Registry Options:**

- All private registry options (`caBundle`, `authHeader`, `clientCert`, `clientKey`, `proxyUrl`) require `sourceUrl`
- `clientCert` and `clientKey` must be specified together for mTLS
- `proxyUrl` must use `http://` or `https://` scheme

## Uninstall Request

```json
{
"version": "1.6.4",
"purge": false
}
```

| Field | Required | Description |
| --------- | -------- | ------------------------------------------------------------------ |
| `version` | No | Version to uninstall (defaults to current version if omitted) |
| `purge` | No | Remove version metadata from database (default: false, keep audit) |

**Notes:**

- Uninstalling the current version switches to the previous version (if available) or clears current
- Blocked if Terraform executions are in progress (when `ExecutionChecker` is configured)
- When `purge: false` (default), version metadata remains with state `Uninstalled` for audit purposes
- When `purge: true`, version metadata is deleted from the database entirely

## Status Response

```json
{
"currentVersion": "1.6.4",
"state": "ready",
"binaryPath": "/terraform/versions/1.6.4/terraform",
"installedAt": "2025-01-06T10:30:00Z",
"source": {
"url": "https://releases.hashicorp.com/terraform/1.6.4/terraform_1.6.4_linux_amd64.zip",
"checksum": "sha256:abc123..."
},
"queue": {
"pending": 0,
"inProgress": null
},
"versions": { ... },
"history": [ ... ],
"lastError": "",
"lastUpdated": "2025-01-06T10:30:00Z"
}
```

| State | Description |
| --------------- | --------------------------------------- |
| `not-installed` | No Terraform version installed |
| `installing` | Installation in progress |
| `ready` | Terraform installed and ready |
| `uninstalling` | Uninstallation in progress |
| `failed` | Last operation failed (see `lastError`) |

## Configuration

| Config Key | Description | Default |
| ------------------------- | ------------------------------------------------- | -------------------------------- |
| `terraform.path` | Root directory for Terraform installations | `/terraform` |
| `terraform.sourceBaseUrl` | Mirror/base URL for downloads (air-gapped setups) | `https://releases.hashicorp.com` |

## Behavior

- **Concurrency:** Only one install/uninstall runs at a time; concurrent requests receive `installer is busy`
- **Archive Detection:** Supports both ZIP archives and plain binaries (detected via magic bytes)
- **Cleanup:** Downloaded archives are automatically removed after extraction
- **Symlink:** Current version is symlinked at `{terraform.path}/current`
10 changes: 8 additions & 2 deletions hack/bicep-types-radius/generated/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,17 @@
"Radius.Core/applications@2025-08-01-preview": {
"$ref": "radius/radius.core/2025-08-01-preview/types.json#/44"
},
"Radius.Core/environments@2025-08-01-preview": {
"Radius.Core/bicepSettings@2025-08-01-preview": {
"$ref": "radius/radius.core/2025-08-01-preview/types.json#/67"
},
"Radius.Core/environments@2025-08-01-preview": {
"$ref": "radius/radius.core/2025-08-01-preview/types.json#/90"
},
"Radius.Core/recipePacks@2025-08-01-preview": {
"$ref": "radius/radius.core/2025-08-01-preview/types.json#/89"
"$ref": "radius/radius.core/2025-08-01-preview/types.json#/112"
},
"Radius.Core/terraformSettings@2025-08-01-preview": {
"$ref": "radius/radius.core/2025-08-01-preview/types.json#/149"
}
},
"resourceFunctions": {},
Expand Down
Loading
Loading