Skip to content
Open
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
26 changes: 26 additions & 0 deletions .github/build/tf-module-server/crd-access.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Note : The Terraform provider for Kubernetes kubernetes_manifest needs to be able to look up the CRD to create a resource of that type.
# https://github.com/hashicorp/terraform-provider-kubernetes/pull/1506
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: radius-dapr-role
rules:
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
- apiGroups: ["dapr.io"]
resources: ["components"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: radius-dapr-rolebinding
subjects:
- kind: ServiceAccount
name: dynamic-rp
namespace: radius-system
roleRef:
kind: ClusterRole
name: radius-dapr-role
apiGroup: rbac.authorization.k8s.io
9 changes: 9 additions & 0 deletions .github/scripts/build-terraform-recipe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ if [[ "$DEPLOYMENT_EXISTS" == "false" ]]; then
-n "$TERRAFORM_MODULE_SERVER_NAMESPACE" --timeout=60s >/dev/null 2>&1; then
echo "Warning: Deployment may not have completed successfully" >&2
fi

echo " Applying ClusterRoleBinding for CRD access..."
CRD_ACCESS_FILE=".github/build/tf-module-server/crd-access.yaml"
if [[ ! -f "$CRD_ACCESS_FILE" ]]; then
echo "Error: CRD access file not found: $CRD_ACCESS_FILE" >&2
exit 1
fi
kubectl apply -f "$CRD_ACCESS_FILE" >/dev/null 2>&1

else
echo " Web server already deployed"
# Restart deployment to pick up ConfigMap changes
Expand Down
4 changes: 4 additions & 0 deletions .github/scripts/create-cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ echo "Installing Radius on Kubernetes..."
rad install kubernetes --set rp.publicEndpointOverride=localhost:8081 --skip-contour-install --set dashboard.enabled=false

echo "✅ Radius installation completed successfully"

echo "Installing Dapr on Kubernetes..."
dapr init --kubernetes
echo "✅ Dapr installation completed successfully"
3 changes: 2 additions & 1 deletion .github/scripts/list-recipe-folders.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ find_recipe_dirs() {
}

# Collect Bicep recipe directories (directories containing .bicep files under recipes/)
find_recipe_dirs -type f -path "*/recipes/*/*.bicep"
# Ignore bicep files under modules/, as those are shared modules, not recipes.
find_recipe_dirs -type f -path "*/recipes/*/*.bicep" -not -path "*/modules/*"

# Collect Terraform recipe directories (directories containing main.tf under recipes/terraform)
find_recipe_dirs -type f -path "*/recipes/*/terraform/main.tf"
Expand Down
46 changes: 29 additions & 17 deletions .github/scripts/test-recipe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,31 +96,43 @@ rad recipe register default \
--template-path "$TEMPLATE_PATH" \
--plain-http

# Check if test file exists
TEST_FILE="$RESOURCE_TYPE_PATH/test/app.bicep"
if [[ ! -f "$TEST_FILE" ]]; then
echo "==> No test file found at $TEST_FILE, skipping deployment test"
# Check for test files matching app*.bicep
TEST_DIR="$RESOURCE_TYPE_PATH/test"
if [[ ! -d "$TEST_DIR" ]]; then
echo "==> Test directory not found at $TEST_DIR, skipping deployment test"
rad recipe unregister default --resource-type "$RESOURCE_TYPE"
exit 0
fi

echo "==> Deploying test application from $TEST_FILE"
APP_NAME="testapp-$(date +%s)"
shopt -s nullglob
TEST_FILES=("$TEST_DIR"/app*.bicep)
shopt -u nullglob

# Deploy the test app
if rad deploy "$TEST_FILE" --application "$APP_NAME" --environment default; then
echo "==> Test deployment successful"

# Cleanup: delete the app
echo "==> Cleaning up test application"
rad app delete "$APP_NAME" --yes
else
echo "==> Test deployment failed"
rad app delete "$APP_NAME" --yes 2>/dev/null || true
if [[ ${#TEST_FILES[@]} -eq 0 ]]; then
echo "==> No test files found matching $TEST_DIR/app*.bicep, skipping deployment test"
rad recipe unregister default --resource-type "$RESOURCE_TYPE"
exit 1
exit 0
fi

for TEST_FILE in "${TEST_FILES[@]}"; do
echo "==> Deploying test application from $TEST_FILE"
APP_NAME="testapp-$(date +%s)"

# Deploy the test app
if rad deploy "$TEST_FILE" --application "$APP_NAME" --environment default; then
echo "==> Test deployment for $TEST_FILE successful"

# Cleanup: delete the app
echo "==> Cleaning up test application $APP_NAME"
rad app delete "$APP_NAME" --yes
else
echo "==> Test deployment failed for $TEST_FILE"
rad app delete "$APP_NAME" --yes 2>/dev/null || true
rad recipe unregister default --resource-type "$RESOURCE_TYPE"
exit 1
fi
done

# Unregister the recipe
echo "==> Unregistering recipe"
rad recipe unregister default --resource-type "$RESOURCE_TYPE"
Expand Down
39 changes: 39 additions & 0 deletions Dapr/common/modules/bicep/types.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@export()
@description('Generic metadata property for a Dapr component')
type DaprMetadata = {
@description('Required. The name of the metadata property')
name: string

@description('Optional. The value of the metadata property')
value: string

@description('Optional. The secret key referencethat contains the value of the metadata property')
secretKeyRef: DaprSecretRef
}

@export()
@description('Reference to a secret key in a dapr secret store')
type DaprSecretRef = {
@description('Required. The name of the secret store')
name: string

@description('Required. The key of the secret')
key: string
}

@export()
@description('Output values for a Dapr component')
type result = {
@description('List of resource IDs created for this component. These will be cleaned up when the component is deleted.')
// This workaround is needed because the deployment engine omits Kubernetes resources from its output.
// This allows Kubernetes resources to be cleaned up when the resource is deleted.
// Once this gap is addressed, users won't need to do this.
resources: string[]
@description('Values exposed by the component when using a connection')
values: {
@description('Dapr component type, e.g. state.redis')
type: string
@description('Dapr component name')
componentName: string
}
}
31 changes: 31 additions & 0 deletions Dapr/stateStores/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Radius.Dapr/stateStores

## Overview

The **Radius.Dapr/stateStores** resource type represents a Dapr state store component. This resource type deploys a Dapr component that can be used by containers with the Dapr sidecar extension enabled.

Developer documentation is embedded in the resource type definition YAML file, and it is accessible via the `rad resource-type show Radius.Dapr/stateStores` command.

## Recipes

A list of available Recipes for this resource type, including links to the Bicep and Terraform templates:

|Platform| IaC Language| Recipe Name | Stage |
|---|---|---|---|
| Kubernetes | Bicep | kubernetes-dapr-statestore.bicep | Alpha |
| Kubernetes | Terraform | main.tf | Alpha |

## Recipe Input Properties

Properties for the **Radius.Dapr/stateStores** resource type are provided via the [Recipe Context](https://docs.radapp.io/reference/context-schema/) object. These properties include:

- `context.properties.type` (string, optional): The type of state store. Defaults to `state.redis` if not provided. See [Dapr state store components](https://docs.dapr.io/reference/components-reference/supported-state-stores/) for available options.
- `context.properties.version` (string, optional): The version of the state store component. Defaults to `v1` if not provided.
- `context.properties.metadata` (array, optional): Metadata specific to the state store component. Each item contains `name`, `value`, and optionally `secretKeyRef`. See [Dapr state store components](https://docs.dapr.io/reference/components-reference/supported-state-stores/) for details.

## Recipe Output Properties

The **Radius.Dapr/stateStores** resource type expects the following output properties to be set in the Results object in the Recipe:

- `context.properties.type` (string): The type of the deployed state store component.
- `context.properties.componentName` (string): The name of the Dapr component that can be used to reference the state store.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { DaprMetadata, result } from '../../../../common/modules/bicep/types.bicep'

extension kubernetes with {
kubeConfig: ''
namespace: context.runtime.kubernetes.namespace
} as kubernetes

@description('Information about what resource is calling this Recipe. Generated by Radius. For more information visit https://docs.radapp.io/reference/context-schema/ ')
param context object

// Inputs
var inputs = context.resource.properties
var type string = inputs.?type ?? 'state.redis'
var version string = inputs.?version ?? 'v1'
var metadata DaprMetadata[] = inputs.?metadata ?? []
var secretStore string = inputs.?secretStore ?? ''

// Radius marking to identify resources it created.
var radiusTags = {
resource: context.resource.name
app: context.application == null ? '' : context.application.name
}
var resourceMetadata = {
name: 'daprstate-${uniqueString(context.resource.id)}'
namespace: context.runtime.kubernetes.namespace
labels: radiusTags
}

/************** DEFAULT STATE STORE ***************/
var defaultComponentType string = 'state.redis'
var redisTag string = '7'
var redisPort int = 6379

resource redis 'apps/Deployment@v1' = if (type == defaultComponentType){
metadata: resourceMetadata
spec: {
selector: { matchLabels: radiusTags }
template: {
metadata: {
labels: union(radiusTags, {
// Label pods with the application name so `rad run` can find the logs.
'radapp.io/application': radiusTags.app
})
}
spec: {
containers: [
{
name: 'redis'
image: 'redis:${redisTag}'
ports: [{ containerPort: redisPort }]
}
]
}
}
}
}

resource svc 'core/Service@v1' = if (type == defaultComponentType) {
metadata: resourceMetadata
spec: {
type: 'ClusterIP'
selector: radiusTags
ports: [{ port: redisPort }]
}
}

var redisResourceIds array = [
'/planes/kubernetes/local/namespaces/${resourceMetadata.namespace}/providers/apps/Deployment/${resourceMetadata.name}'
'/planes/kubernetes/local/namespaces/${resourceMetadata.namespace}/providers/core/Service/${resourceMetadata.name}'
]

var redisMetadata array = [
{
name: 'redisHost'
value: '${resourceMetadata.name}.${resourceMetadata.namespace}.svc.cluster.local:${redisPort}'
}
{
name: 'redisPassword'
value: ''
}
]


/************** DAPR COMPONENT ***************/
#disable-next-line BCP081
resource daprComponent 'dapr.io/Component@v1alpha1' = {
metadata: resourceMetadata
auth: { secretStore: secretStore }
spec: {
type: type
version: version
metadata: concat(
type == defaultComponentType ? redisMetadata : [],
metadata
)
}
}

output result result = {
resources: concat(
[
'/planes/kubernetes/local/namespaces/${context.runtime.kubernetes.namespace}/providers/dapr.io/Component/${daprComponent.metadata.name}'
],
type == defaultComponentType ? redisResourceIds : []
)
values: {
type: type
componentName: daprComponent.metadata.name
}
}
82 changes: 82 additions & 0 deletions Dapr/stateStores/recipes/kubernetes/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
terraform {
required_version = ">= 1.5"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.37.1"
}
}
}

locals {
# Recipes inputs (doesn't use variables directly)
inputs = var.context.resource.properties
default_type = "state.redis"
type = try(local.inputs.type, local.default_type)
version = try(local.inputs.version, "v1")
metadata = try(local.inputs.metadata, [])
secretStore = try(local.inputs.secretStore, "")

# Env context
namespace = var.context.runtime.kubernetes.namespace
application_name = try(var.context.application.name, "")

# Computed values
resource_name = "daprstate-${substr(md5(var.context.resource.id), 0, 13)}"
radius_tags = {
resource = var.context.resource.name
// To Review : Are the two of them necessary?
app = local.application_name
"radapp.io/application" = local.application_name
}
computed_metadata = concat(try(module.default_state_store[0].metadata, []), local.metadata)

# Output values
dapr_component_id = "/planes/kubernetes/local/namespaces/${local.namespace}/providers/dapr.io/Component/${local.resource_name}"
resource_ids = concat([local.dapr_component_id], try(module.default_state_store[0].resource_ids, []))
}

module "default_state_store" {
source = "./modules/redis"
count = local.type == local.default_type ? 1 : 0

name = local.resource_name
namespace = local.namespace
tags = local.radius_tags
}

resource "kubernetes_manifest" "dapr_component" {
manifest = {
apiVersion = "dapr.io/v1alpha1"
kind = "Component"
metadata = {
name = local.resource_name
namespace = local.namespace
labels = local.radius_tags
}
auth = {
secretStore = local.secretStore
}
spec = {
type = local.type
version = local.version
metadata = [
for entry in local.computed_metadata : merge(
{ name = entry.name },
try(entry.value, null) != null ? { value = entry.value } : {},
try(entry.secretKeyRef, null) != null ? { secretKeyRef = entry.secretKeyRef } : {}
)
]
}
}
}

output "result" {
value = {
resources = local.resource_ids
values = {
type = local.type
componentName = local.resource_name
}
}
}
Loading