From fd23eb05224651e0a038b757a2c3cdd371a1966b Mon Sep 17 00:00:00 2001 From: SoTrx <11771975+SoTrx@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:26:43 +0200 Subject: [PATCH 1/2] feat: Add Dapr.Statestore Signed-off-by: SoTrx <11771975+SoTrx@users.noreply.github.com> --- .devcontainer/devcontainer.json | 41 ++++++ .devcontainer/post-create.sh | 6 + .devcontainer/post-start.sh | 7 ++ .../build/tf-module-server/crd-access.yaml | 26 ++++ .github/scripts/build-terraform-recipe.sh | 9 ++ .github/scripts/create-cluster.sh | 4 + .github/scripts/list-recipe-folders.sh | 3 +- .github/scripts/test-recipe.sh | 46 ++++--- Dapr/common/modules/bicep/types.bicep | 39 ++++++ Dapr/stateStores/README.md | 31 +++++ .../bicep/kubernetes-dapr-statestore.bicep | 110 ++++++++++++++++ .../recipes/kubernetes/terraform/main.tf | 82 ++++++++++++ .../terraform/modules/redis/main.tf | 57 +++++++++ .../terraform/modules/redis/outputs.tf | 21 ++++ .../terraform/modules/redis/variables.tf | 26 ++++ .../recipes/kubernetes/terraform/var.tf | 4 + Dapr/stateStores/stateStores.yaml | 117 ++++++++++++++++++ Dapr/stateStores/test/app-default.bicep | 52 ++++++++ Dapr/stateStores/test/app-manual.bicep | 81 ++++++++++++ .../testing-resource-types-recipes.md | 1 + 20 files changed, 745 insertions(+), 18 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/post-create.sh create mode 100644 .devcontainer/post-start.sh create mode 100644 .github/build/tf-module-server/crd-access.yaml create mode 100644 Dapr/common/modules/bicep/types.bicep create mode 100644 Dapr/stateStores/README.md create mode 100644 Dapr/stateStores/recipes/kubernetes/bicep/kubernetes-dapr-statestore.bicep create mode 100644 Dapr/stateStores/recipes/kubernetes/terraform/main.tf create mode 100644 Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/main.tf create mode 100644 Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/outputs.tf create mode 100644 Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/variables.tf create mode 100644 Dapr/stateStores/recipes/kubernetes/terraform/var.tf create mode 100644 Dapr/stateStores/stateStores.yaml create mode 100644 Dapr/stateStores/test/app-default.bicep create mode 100644 Dapr/stateStores/test/app-manual.bicep diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..e5801967 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +{ + "name": "Radius Resource Types & Recipes", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm", + + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": {}, + // Note : k3d doesn't run too nicely with docker-in-docker for some reason + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, + "ghcr.io/devcontainers/features/terraform:1": {}, + "ghcr.io/jsburckhardt/devcontainer-features/k3d:1": {}, + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { + "minikube": "none", + "helm": "none", + "kubectl": "latest" + }, + "ghcr.io/dhoeric/features/oras:1": {}, + "ghcr.io/dapr/cli/dapr-cli:0": {}, + "ghcr.io/devcontainers-extra/features/apt-packages:1": { + "packages": "socat" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-bicep", + "hashicorp.terraform", + "ms-kubernetes-tools.vscode-kubernetes-tools", + "ms-vscode.azure-account", + "github.copilot", + "github.copilot-chat", + "redhat.vscode-yaml", + "ms-vscode.makefile-tools", + "davidanson.vscode-markdownlint", + "ms-vscode.vscode-json" + ] + } + }, + + "postCreateCommand": "bash .devcontainer/post-create.sh", + "postStartCommand": "bash .devcontainer/post-start.sh" +} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000..0cd1c9ee --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eo pipefail + +# Aliases +touch ~/.bash_aliases +echo "alias k=kubectl" >> ~/.bash_aliases \ No newline at end of file diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100644 index 00000000..e23b3e85 --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eo pipefail + +# Note: Using docker outside of docker means the oras server will run on the host docker daemon. +# Thus the "localhost:5000" used in the build scripts will not work inside the devcontainer, it should be :5000. +# We can use host.docker.internal to access the host from inside the container. We'll proxy the port using socat. +exec socat TCP-LISTEN:5000,reuseaddr,fork TCP:host.docker.internal:5000 diff --git a/.github/build/tf-module-server/crd-access.yaml b/.github/build/tf-module-server/crd-access.yaml new file mode 100644 index 00000000..a069908d --- /dev/null +++ b/.github/build/tf-module-server/crd-access.yaml @@ -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 diff --git a/.github/scripts/build-terraform-recipe.sh b/.github/scripts/build-terraform-recipe.sh index 4930bab5..eb4e3d8f 100755 --- a/.github/scripts/build-terraform-recipe.sh +++ b/.github/scripts/build-terraform-recipe.sh @@ -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 diff --git a/.github/scripts/create-cluster.sh b/.github/scripts/create-cluster.sh index 872c147c..36e1dc75 100755 --- a/.github/scripts/create-cluster.sh +++ b/.github/scripts/create-cluster.sh @@ -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" diff --git a/.github/scripts/list-recipe-folders.sh b/.github/scripts/list-recipe-folders.sh index c9773956..215b9063 100755 --- a/.github/scripts/list-recipe-folders.sh +++ b/.github/scripts/list-recipe-folders.sh @@ -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" diff --git a/.github/scripts/test-recipe.sh b/.github/scripts/test-recipe.sh index e3f83c48..a7ec95a0 100755 --- a/.github/scripts/test-recipe.sh +++ b/.github/scripts/test-recipe.sh @@ -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" diff --git a/Dapr/common/modules/bicep/types.bicep b/Dapr/common/modules/bicep/types.bicep new file mode 100644 index 00000000..f9eb4725 --- /dev/null +++ b/Dapr/common/modules/bicep/types.bicep @@ -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 + } +} diff --git a/Dapr/stateStores/README.md b/Dapr/stateStores/README.md new file mode 100644 index 00000000..c321ab15 --- /dev/null +++ b/Dapr/stateStores/README.md @@ -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. diff --git a/Dapr/stateStores/recipes/kubernetes/bicep/kubernetes-dapr-statestore.bicep b/Dapr/stateStores/recipes/kubernetes/bicep/kubernetes-dapr-statestore.bicep new file mode 100644 index 00000000..67ee196b --- /dev/null +++ b/Dapr/stateStores/recipes/kubernetes/bicep/kubernetes-dapr-statestore.bicep @@ -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 + } +} diff --git a/Dapr/stateStores/recipes/kubernetes/terraform/main.tf b/Dapr/stateStores/recipes/kubernetes/terraform/main.tf new file mode 100644 index 00000000..7648c113 --- /dev/null +++ b/Dapr/stateStores/recipes/kubernetes/terraform/main.tf @@ -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 + } + } +} diff --git a/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/main.tf b/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/main.tf new file mode 100644 index 00000000..27efc506 --- /dev/null +++ b/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/main.tf @@ -0,0 +1,57 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.37.1" + } + } +} + +resource "kubernetes_deployment" "redis" { + metadata { + name = var.name + namespace = var.namespace + labels = var.tags + } + + spec { + selector { + match_labels = var.tags + } + + template { + metadata { + labels = var.tags + } + + spec { + container { + name = "redis" + image = "redis:${var.redis_image_tag}" + + port { + container_port = var.redis_port + } + } + } + } + } +} + +resource "kubernetes_service" "redis" { + metadata { + name = var.name + namespace = var.namespace + labels = var.tags + } + + spec { + selector = var.tags + + port { + port = var.redis_port + } + + type = "ClusterIP" + } +} diff --git a/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/outputs.tf b/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/outputs.tf new file mode 100644 index 00000000..8296b823 --- /dev/null +++ b/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/outputs.tf @@ -0,0 +1,21 @@ +output "resource_ids" { + description = "List of Kubernetes resource IDs created by this module" + value = [ + "/planes/kubernetes/local/namespaces/${var.namespace}/providers/apps/Deployment/${var.name}", + "/planes/kubernetes/local/namespaces/${var.namespace}/providers/core/Service/${var.name}" + ] +} + +output "metadata" { + description = "Dapr component metadata for Redis state store" + value = [ + { + name = "redisHost" + value = "${var.name}.${var.namespace}.svc.cluster.local:${var.redis_port}" + }, + { + name = "redisPassword" + value = "" + } + ] +} diff --git a/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/variables.tf b/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/variables.tf new file mode 100644 index 00000000..cb3f6a4a --- /dev/null +++ b/Dapr/stateStores/recipes/kubernetes/terraform/modules/redis/variables.tf @@ -0,0 +1,26 @@ +variable "name" { + description = "Name for the Redis deployment and service" + type = string +} + +variable "namespace" { + description = "Kubernetes namespace" + type = string +} + +variable "tags" { + description = "Tags to apply to Kubernetes resources" + type = map(string) +} + +variable "redis_image_tag" { + description = "Redis image tag" + type = string + default = "7" +} + +variable "redis_port" { + description = "Redis port" + type = number + default = 6379 +} diff --git a/Dapr/stateStores/recipes/kubernetes/terraform/var.tf b/Dapr/stateStores/recipes/kubernetes/terraform/var.tf new file mode 100644 index 00000000..aaa15d17 --- /dev/null +++ b/Dapr/stateStores/recipes/kubernetes/terraform/var.tf @@ -0,0 +1,4 @@ +variable "context" { + description = "Information about what resource is calling this Recipe. Generated by Radius." + type = any +} \ No newline at end of file diff --git a/Dapr/stateStores/stateStores.yaml b/Dapr/stateStores/stateStores.yaml new file mode 100644 index 00000000..08dcb489 --- /dev/null +++ b/Dapr/stateStores/stateStores.yaml @@ -0,0 +1,117 @@ +namespace: Radius.Dapr +types: + stateStores: + description: | + The Radius.Dapr/stateStores Resource Type deploys a Dapr state store component for your application. To provision a state store, add the `stateStores` resource to your application definition Bicep file. + + extension radius + extension stateStores + param environment string + + resource myApplication 'Applications.Core/applications@2023-10-01-preview' = { ... } + + resource stateStore 'Radius.Dapr/stateStores@2025-11-01-preview' = { + name: 'statestore' + properties: { + application: myApplication.id + environment: environment + type: 'state.redis' + metadata: [ + { + name: 'redisHost' + value: 'redis:6379' + } + ] + } + } + + To use the state store from a container, connect the container resource to the state store and enable the Dapr sidecar extension. + + resource backend 'Applications.Core/containers@2023-10-01-preview' = { + name: 'backend' + properties: { + application: myApplication.id + environment: environment + container: { + image: 'ghcr.io/radius-project/samples/dapr-backend:latest' + ports: { + orders: { + containerPort: 3000 + } + } + } + connections: { + orders: { + source: stateStore.id + } + } + extensions: [ + { + kind: 'daprSidecar' + appId: 'backend' + appPort: 3000 + } + ] + } + } + + The connection deploys the Dapr component and wires it to the container's sidecar. It also injects environment variables into the container for the connection values. With the connection name `orders`, the following variables are available: + + CONNECTION_ORDERS_TYPE + CONNECTION_ORDERS_COMPONENTNAME + + apiVersions: + '2025-11-01-preview': + schema: + type: object + properties: + # Radius specific properties + environment: + type: string + description: "(Required) The Radius EnvironmentID. Typically set by the rad CLI. Typically value should be `environment`" + application: + type: string + description: "(Optional) The Radius Application ID. `myApplication.id` for example." + # Resource type specific properties + type: + type: string + description: "(Optional) The type of state store. Default is `state.redis`. See https://docs.dapr.io/reference/components-reference/supported-state-stores/ for details." + version: + type: string + description: "(Optional) The version of the state store component. Default is `v1`." + metadata: + type: array + description: "(Optional) Metadata specific to the state store component. See https://docs.dapr.io/reference/components-reference/supported-state-stores/ for details." + items: + type: object + properties: + name: + type: string + description: "(Required) Name of the metadata property." + value: + type: string + description: "(Optional) Value of the metadata property. Leave empty if using secretKeyRef." + secretKeyRef: + type: object + description: "(Optional) Reference to a secret key in a dapr secret store. Leave empty if using value." + properties: + name: + type: string + description: "(Required) The name of the secret store." + key: + type: string + description: "(Required) The key of the secret in the secret store." + required: + - name + - key + required: + - name + secretStore: + type: string + description: "(Optional) The name of the dapr secret store to use for this component." + componentName: + type: string + readOnly: true + description: "The name of the Dapr component that can be used to reference the state store." + required: + - environment \ No newline at end of file diff --git a/Dapr/stateStores/test/app-default.bicep b/Dapr/stateStores/test/app-default.bicep new file mode 100644 index 00000000..d10c76fd --- /dev/null +++ b/Dapr/stateStores/test/app-default.bicep @@ -0,0 +1,52 @@ +extension radius +extension stateStores + +@description('Specifies the environment for resources.') +param environment string + +param application string + +resource backend 'Applications.Core/containers@2023-10-01-preview' = { + name: 'backend-${uniqueString(application)}' + properties: { + application: application + container: { + image: 'ghcr.io/radius-project/samples/dapr-backend:latest' + ports: { + orders: { + containerPort: 3000 + } + } + readinessProbe:{ + kind:'httpGet' + containerPort:3000 + path: '/order' + initialDelaySeconds:3 + failureThreshold:4 + periodSeconds:20 + } + } + connections: { + orders: { + source: stateStore.id + } + } + extensions: [ + { + kind: 'daprSidecar' + appId: 'backend' + appPort: 3000 + } + ] + } +} + +// The Dapr state store that is connected to the backend container +resource stateStore 'Radius.Dapr/stateStores@2025-11-01-preview' = { + name: 'stateStore-${uniqueString(application)}' + properties: { + // Provision Redis Dapr state store automatically via the default Radius Recipe + environment: environment + application: application + } +} diff --git a/Dapr/stateStores/test/app-manual.bicep b/Dapr/stateStores/test/app-manual.bicep new file mode 100644 index 00000000..8c65bcf3 --- /dev/null +++ b/Dapr/stateStores/test/app-manual.bicep @@ -0,0 +1,81 @@ +extension radius +extension stateStores + +@description('Specifies the environment for resources.') +param environment string + +param application string + +resource backend 'Applications.Core/containers@2023-10-01-preview' = { + name: 'backend-${uniqueString(application)}' + properties: { + application: application + container: { + image: 'ghcr.io/radius-project/samples/dapr-backend:latest' + ports: { + orders: { + containerPort: 3000 + } + } + } + connections: { + orders: { + source: stateStore.id + } + } + extensions: [ + { + kind: 'daprSidecar' + appId: 'backend' + appPort: 3000 + } + ] + } +} +var pgHost = 'postgres-${uniqueString(application)}' +resource postgres 'Applications.Core/containers@2023-10-01-preview' = { + name: pgHost + properties: { + application: application + container: { + image: 'postgres:18-alpine' + ports: { + postgres: { + containerPort: 5432 + } + } + env: { + POSTGRES_USER: { + value: 'dapr' + } + POSTGRES_PASSWORD: { + value: 'password' + } + POSTGRES_DB: { + value: 'daprStore' + } + } + } + } +} + +// The Dapr state store that is connected to the backend container +resource stateStore 'Radius.Dapr/stateStores@2025-11-01-preview' = { + name: 'backend-${uniqueString(application)}' + dependsOn: [ + postgres + ] + properties: { + environment: environment + application: application + type: 'state.postgresql' + // This is one of the few components having multiple versions + version: 'v2' + metadata: [ + { + name: 'connectionString' + value: 'host=${pgHost} user=dapr password=password dbname=daprStore port=5432 sslmode=disable' + } + ] + } +} diff --git a/docs/contributing/testing-resource-types-recipes.md b/docs/contributing/testing-resource-types-recipes.md index dfe6a93d..215f3d9a 100644 --- a/docs/contributing/testing-resource-types-recipes.md +++ b/docs/contributing/testing-resource-types-recipes.md @@ -9,6 +9,7 @@ Before testing, ensure you have: - Docker installed (for running k3d) - `kubectl` installed - `make` available in your environment +- `dapr` CLI installed (for Dapr related recipes) ## Quick Start From 77b24518591f31881fa0887042df4d91e231060f Mon Sep 17 00:00:00 2001 From: SoTrx <11771975+SoTrx@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:55:43 +0200 Subject: [PATCH 2/2] Remove devcontainer Signed-off-by: SoTrx <11771975+SoTrx@users.noreply.github.com> --- .devcontainer/devcontainer.json | 41 --------------------------------- .devcontainer/post-create.sh | 6 ----- .devcontainer/post-start.sh | 7 ------ 3 files changed, 54 deletions(-) delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/post-create.sh delete mode 100644 .devcontainer/post-start.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index e5801967..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "Radius Resource Types & Recipes", - "image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm", - - "features": { - "ghcr.io/devcontainers/features/azure-cli:1": {}, - // Note : k3d doesn't run too nicely with docker-in-docker for some reason - "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, - "ghcr.io/devcontainers/features/terraform:1": {}, - "ghcr.io/jsburckhardt/devcontainer-features/k3d:1": {}, - "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { - "minikube": "none", - "helm": "none", - "kubectl": "latest" - }, - "ghcr.io/dhoeric/features/oras:1": {}, - "ghcr.io/dapr/cli/dapr-cli:0": {}, - "ghcr.io/devcontainers-extra/features/apt-packages:1": { - "packages": "socat" - } - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-azuretools.vscode-bicep", - "hashicorp.terraform", - "ms-kubernetes-tools.vscode-kubernetes-tools", - "ms-vscode.azure-account", - "github.copilot", - "github.copilot-chat", - "redhat.vscode-yaml", - "ms-vscode.makefile-tools", - "davidanson.vscode-markdownlint", - "ms-vscode.vscode-json" - ] - } - }, - - "postCreateCommand": "bash .devcontainer/post-create.sh", - "postStartCommand": "bash .devcontainer/post-start.sh" -} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100644 index 0cd1c9ee..00000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Aliases -touch ~/.bash_aliases -echo "alias k=kubectl" >> ~/.bash_aliases \ No newline at end of file diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh deleted file mode 100644 index e23b3e85..00000000 --- a/.devcontainer/post-start.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -eo pipefail - -# Note: Using docker outside of docker means the oras server will run on the host docker daemon. -# Thus the "localhost:5000" used in the build scripts will not work inside the devcontainer, it should be :5000. -# We can use host.docker.internal to access the host from inside the container. We'll proxy the port using socat. -exec socat TCP-LISTEN:5000,reuseaddr,fork TCP:host.docker.internal:5000