diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 33e48b42..8f4c7bd0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,3 +7,6 @@ # Default rule: anything that doesn't match a more specific rule goes here * @radius-project/maintainers-resource-types-contrib @radius-project/approvers-resource-types-contrib + +# Allows on-call members to respond to dependabot updates to workflows. +.github/workflows/* @radius-project/on-call @radius-project/maintainers-resource-types-contrib @radius-project/approvers-resource-types-contrib diff --git a/build/help.mk b/.github/build/help.mk similarity index 100% rename from build/help.mk rename to .github/build/help.mk diff --git a/build/tf-module-server/resources.yaml b/.github/build/tf-module-server/resources.yaml similarity index 94% rename from build/tf-module-server/resources.yaml rename to .github/build/tf-module-server/resources.yaml index c7dcb7b0..9ac8c659 100644 --- a/build/tf-module-server/resources.yaml +++ b/.github/build/tf-module-server/resources.yaml @@ -17,7 +17,7 @@ spec: containers: - image: mcr.microsoft.com/azurelinux/base/nginx:1.25 name: nginx - # nginx will serve files found in this directory. + # nginx will serve files found in this directory volumeMounts: - name: content mountPath: /usr/share/nginx/html diff --git a/build/validation.mk b/.github/build/validation.mk similarity index 98% rename from build/validation.mk rename to .github/build/validation.mk index 1a6fbb40..79b9c416 100644 --- a/build/validation.mk +++ b/.github/build/validation.mk @@ -44,7 +44,7 @@ publish-test-terraform-recipes: ## Publishes terraform recipes to the current Ku rm -rf "$$temp_recipes_dir" @echo -e "$(ARROW) Deploying web server..." - kubectl apply -f ./build/tf-module-server/resources.yaml -n $(TERRAFORM_MODULE_SERVER_NAMESPACE) + kubectl apply -f ./.github/build/tf-module-server/resources.yaml -n $(TERRAFORM_MODULE_SERVER_NAMESPACE) @echo -e "$(ARROW) Waiting for web server to be ready..." kubectl rollout status deployment.apps/tf-module-server -n $(TERRAFORM_MODULE_SERVER_NAMESPACE) --timeout=600s diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..393b431e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## Description + + +Related GitHub Issue: + +## Testing + + +## Contributor Checklist + +- [ ] File names follow naming conventions and folder structure +- [ ] Platform engineer documentation is in README.md +- [ ] Developer documentation is the top-level description property +- [ ] Example of defining the Resource Type is in the developer documentation +- [ ] Example of using the Resource Type with a Container is in the developer documentation +- [ ] Verified the output of `rad resource-type show` is correct +- [ ] All properties in the Resource Type definition have clear descriptions +- [ ] Enum properties have values defined in `enum: []` +- [ ] Required properties are listed in `required: []` for every object property (not just the top-level properties) +- [ ] Properties about the deployed resource, such as connection strings, are defined as read-only properties and are marked as `readOnly: true` +- [ ] Recipes include a results output variable with all read-only properties set +- [ ] Environment-specific parameters, such as a vnet ID, are exposed for platform engineers to set in the Environment +- [ ] Recipes use the [Recipe context object](https://docs.radapp.io/reference/context-schema/) when possible +- [ ] Recipes are provided for at least one platform +- [ ] Recipes handle secrets securely +- [ ] Recipes are idempotent +- [ ] Resource types and recipes were tested diff --git a/.github/scripts/update-bicepconfig.sh b/.github/scripts/update-bicepconfig.sh index 1bbd37ac..e10b8e8b 100755 --- a/.github/scripts/update-bicepconfig.sh +++ b/.github/scripts/update-bicepconfig.sh @@ -1,10 +1,19 @@ #!/bin/bash set -e -# Script: Update bicepconfig.json with published extensions -# This script finds all published .tgz files and adds them to bicepconfig.json +# This script creates a fresh bicepconfig.json with all published .tgz files -echo "Updating bicepconfig.json with published extensions..." +echo "Creating bicepconfig.json with published extensions..." + +# Create base bicepconfig.json with required experimental features +cat > bicepconfig.json << 'EOF' +{ + "extensions": { + "radius": "br:biceptypes.azurecr.io/radius:latest", + "aws": "br:biceptypes.azurecr.io/aws:latest" + } +} +EOF # Find all published .tgz files and add them to bicepconfig.json (safe handling) tgz_files=() @@ -29,7 +38,7 @@ if [[ ${#tgz_files[@]} -gt 0 ]]; then mv bicepconfig.tmp bicepconfig.json done - echo "✅ Successfully updated bicepconfig.json with extensions" + echo "✅ Successfully created bicepconfig.json with extensions" else echo "No extension .tgz files found to add to bicepconfig.json" fi diff --git a/.github/scripts/validate-common.sh b/.github/scripts/validate-common.sh index 681a1e61..89507c9d 100755 --- a/.github/scripts/validate-common.sh +++ b/.github/scripts/validate-common.sh @@ -32,14 +32,21 @@ find_yaml_files() { printf '%s\n' "${yaml_files[@]}" } -# Find recipe files with specific pattern +# Find recipe files with specific pattern (only in configured resource folders) find_recipe_files() { local pattern="$1" local recipe_files=() - while IFS= read -r -d '' f; do - recipe_files+=("$f") - done < <(find . -path "$pattern" -type f -print0) + for folder in "${resource_folders[@]}"; do + if [[ -d "./$folder" ]]; then + echo "Searching for recipes in folder: $folder" >&2 + while IFS= read -r -d '' f; do + recipe_files+=("$f") + done < <(find "./$folder" -path "$pattern" -type f -print0) + else + echo "Folder $folder does not exist, skipping recipe search..." >&2 + fi + done printf '%s\n' "${recipe_files[@]}" } @@ -342,7 +349,7 @@ test_recipes() { done # Deploy test application for this resource type - test_app_path="$root_folder/$resource_type/app.bicep" + test_app_path="$root_folder/$resource_type/test/app.bicep" deployment_name="test-${root_folder,,}-${platform_service}-${template_kind}-$(date +%s)" deploy_and_cleanup_test_app "$test_app_path" "$deployment_name" "for $platform_service ($template_kind recipe)" diff --git a/.github/workflows/validate-resource-types.yaml b/.github/workflows/validate-resource-types.yaml index a59366ff..43e62e00 100644 --- a/.github/workflows/validate-resource-types.yaml +++ b/.github/workflows/validate-resource-types.yaml @@ -3,8 +3,12 @@ name: Validate Resource Types on: push: branches: [ main ] + paths: + - '**/test/**' pull_request: branches: [ main ] + paths: + - '**/test/**' workflow_dispatch: inputs: version: @@ -20,9 +24,9 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version: 16 - name: Download k3d diff --git a/.gitignore b/.gitignore index fe2ed02e..df6e51c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -./*.tgz \ No newline at end of file +**/*.tgz +**/bicepconfig.json \ No newline at end of file diff --git a/Compute/routes/README.md b/Compute/routes/README.md new file mode 100644 index 00000000..40176bb1 --- /dev/null +++ b/Compute/routes/README.md @@ -0,0 +1,38 @@ +## Overview + +The Radius.Compute/routes Resource Type defines network routes for responding to external clients. It is always part of a Radius Application. It is analogous to a Kubernetes HTTPRoute, TCPRoute, TLSRoute, or UDPRoute resource. + +Developer documentation is embedded in the Resource Type definition YAML file. Developer documentation is accessible via `rad resource-type show Radius.Compute/routes`. + +## Recipes + +A list of available Recipes for this Resource Type, including links to the Bicep and Terraform templates: + +|Platform| IaC Language| Recipe Name | Stage | +|---|---|---|---| +| TODO | TODO | TODO | Alpha | + +## Recipe Input Properties + +| Radius Property | Kubernetes Property | +|---|---| +| context.properties.kind | Used by Recipe to determine which Kubernetes resource type to create (HTTPRoute, TCPRoute, TLSRoute, or UDPRoute). | +| context.properties.hostnames[] | HTTPRoute.spec.hostnames[] | +| context.properties.rules[] | HTTPRoute.spec.rules[] | +| context.properties.rules[].matches[] | HTTPRoute.spec.rules[].matches[] | +| context.properties.rules[].matches[].httpHeaders[].* | HTTPRoute.spec.rules[].matches[].headers[].* | +| context.properties.rules[].matches[].httpMethod | HTTPRoute.spec.rules[].matches[].method | +| context.properties.rules[].matches[].httpPath | HTTPRoute.spec.rules[].matches[].path.value | +| context.properties.rules[].matches[].httpQueryParams[].* | HTTPRoute.spec.rules[].matches[].queryParams[].* | +| context.properties.rules[].destinationContainer | N/A | +| context.properties.rules[].destinationContainer.resourceId | N/A | +| context.properties.rules[].destinationContainer.containerName | N/A | +| context.properties.rules[].destinationContainer.containerPortName | N/A | + +## Recipe Output Properties + +### result.listener + +- result.listener.hostname: The hostname of the listener. The listener hostname plus the paths defined by the developer constitute the URL. +- result.listener.port: The port of the listener +- result.listener.protocol: The protocol of the listener \ No newline at end of file diff --git a/Compute/routes/routes.yaml b/Compute/routes/routes.yaml new file mode 100644 index 00000000..56dd89da --- /dev/null +++ b/Compute/routes/routes.yaml @@ -0,0 +1,193 @@ +namespace: Radius.Compute +types: + routes: + description: | + The Radius.Compute/routes Resource Type defines network routes for responding to external clients. Note that a Routes resource is not required for service-to-service communication. To use Routes, define a Container and ensure a `containerPort` is specified. + + extension radius + param environment string + + resource myApplication 'Radius.Core/applications@2025-08-01-preview' = { ... } + + resource myContainer 'Radius.Compute/containers@2025-08-01-preview' = { + name: 'myContainer' + properties: { + environment: environment + application: myApplication.id + containers: { + frontend: { + image: 'frontend:1.25' + ports: { + web: { + containerPort: 8080 + } + } + } + accounts: { + image: 'accounts:1.25' + ports: { + web: { + containerPort: 8080 + } + } + } + } + } + } + + Then define a Routes resource. + + resource ingressRule 'Radius.Compute/routes@2025-08-01-preview' = { + name: 'ingressRule' + properties: { + application: myApplication.id + environment: environment + kind: 'HTTP' + rules: [ + { + matches: [ + { + httpPath: '/' + } + ] + destinationContainer: { + resourceId: myContainer.id + containerName: 'frontend' + containerPortName: 'web' + } + } + ] + } + } + + The hostname is determined by the Recipe. + + Multiple rules can be included in Routes. + + resource ingressRule 'Radius.Compute/routes@2025-08-01-preview' = { + name: 'ingressRule' + properties: { + application: myApplication.id + environment: environment + kind: 'HTTP' + rules: [ + { + matches: [ + { + httpPath: '/' + } + ] + destinationContainer: { + resourceId: myContainer.id + containerName: 'frontend' + containerPortName: 'web' + } + } + { + matches: [ + { + httpPath: '/accounts' + } + ] + destinationContainer: { + resourceId: myContainer.id + containerName: 'accounts' + containerPortName: 'web' + } + } + ] + } + } + + apiVersions: + '2025-08-01-preview': + schema: + type: object + properties: + environment: + type: string + description: (Required) The Radius Environment ID. Typically set by the rad CLI. Typically value should be `environment`. + application: + type: string + description: (Required) The Radius Application ID. `myApplication.id` for example. + kind: + type: string + enum: [HTTP, TCP, TLS, UDP] + description: (Optional) The type of rule. If not specified, `HTTPRoute` is assumed. `HTTPRoute` provides L7 ingress with support for matching based on the hostname and HTTP header. `TCPRoute` provides L4 ingress with no support for matching (all traffic is forwarded to the Container). `TLSRoute` provides L4 ingress with the ability to match based on Server Name Indication (SNI) which is equivalent to hostname in TLS. `UDPRoute` is similar to TCPRoutes but uses UDP. + hostnames: + type: array + description: (Optional) Use only when kind is HTTP or TLS. When HTTP, match against the HTTP Host header. When using TLS, match against the SNI attribute of TLS ClientHello message. Hostname may be preceded by a * wildcard. + items: + type: string + rules: + type: array + description: (Required) Rules define semantics for matching a network connection request based on conditions and forwarding the request to a Container. + items: + type: object + properties: + matches: + type: array + description: (Required) Matches define conditions used for matching a request. + items: + type: object + properties: + httpHeaders: + type: array + description: (Optional) HTTP headers to match. Specify only when kind is HTTP. Multiple match values are ANDed together. A request must match all the specified headers to match. + items: + type: object + properties: + name: + type: string + description: (Required) The name of the HTTP Header to be matched. Must be exact. + value: + type: string + description: (Required) Value of HTTP Header to be matched. + required: [name, value] + httpMethod: + type: string + description: (Optional) The HTTP method to match. Specify only when kind is HTTP. + enum: [GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH] + httpPath: + type: string + description: (Optional) The HTTP request path to match. Trailing space is ignored. Requests for `/abc`, `/abc/`, and ``/abc/def/` will all match `/abc`. + httpQueryParams: + type: array + description: (Optional) HTTP query parameters to match. Specify only when kind is HTTP. + items: + type: object + properties: + name: + type: string + description: (Required) Name of the HTTP query parameter to be matched. Specify only when kind is HTTP. + value: + type: string + description: (Required) Value of the HTTP query parameter to be matched. + required: [name, value] + destinationContainer: + type: object + properties: + resourceId: + type: string + description: (Required) The Radius Container resource ID. + containerName: + type: string + description: (Required) The specific container to target within the Container resource. + containerPortName: + type: string + description: (Required) The port name to target from the container. + required: [resourceId, containerName, containerPortName] + required: [matches, destinationContainer] + listener: + type: object + description: (Read Only) The Gateway Listener the route is assigned to. + readOnly: true + properties: + hostname: + type: string + port: + type: integer + protocol: + type: string + enum: [HTTP, HTTPS, TLS, TCP, UDP] + required: [environment, application, rules] \ No newline at end of file diff --git a/Data/mySqlDatabases/README.md b/Data/mySqlDatabases/README.md new file mode 100644 index 00000000..409ecaa5 --- /dev/null +++ b/Data/mySqlDatabases/README.md @@ -0,0 +1,33 @@ +# Radius.Data/mySqlDatabases + +## Overview + +The **Radius.Data/mySqlDatabases** resource type represents a MySQL database. It allows developers to create and easily connect to a MySQL database as part of their Radius applications. + +Developer documentation is embedded in the resource type definition YAML file, and it is accessible via the `rad resource-type show Radius.Data/mySqlDatabases` 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-mysql.bicep | Alpha | + +## Recipe Input Properties + +Properties for the **Radius.Data/mySqlDatabases** resource type are provided via the [Recipe Context](https://docs.radapp.io/reference/context-schema/) object. These properties include: + +- `context.properties.database`(string, optional): The name of the database. Defaults to the `application-name` if not provided. +- `context.properties.username`(string, optional): The username for connecting to the database. Defaults to the `application-name-user` if not provided. +- `context.properties.version`(string, optional): The major MySQL server version in the X.Y format. Defaults to the version `8.4` if not provided. + +## Recipe Output Properties + +The **Radius.Data/mySqlDatabases** resource type expects the following output properties to be set in the Results object in the Recipe: + +- `context.properties.host` (string): The hostname used to connect to the database. +- `context.properties.port` (integer): The port number used to connect to the database. +- `context.properties.database` (string): The name of the database. +- `context.properties.username` (string): The username for connecting to the database. +- `context.properties.password` (string): The password for connecting to the database. diff --git a/Data/mySqlDatabases/mySqlDatabases.yaml b/Data/mySqlDatabases/mySqlDatabases.yaml new file mode 100644 index 00000000..b0c807fe --- /dev/null +++ b/Data/mySqlDatabases/mySqlDatabases.yaml @@ -0,0 +1,86 @@ +namespace: Radius.Data +types: + mySqlDatabases: + description: | + The Radius.Data/mySqlDatabases Resource Type deploys a MySQL database. To deploy a new MySQL database, add the mySqlDatabases resource to the application definition Bicep file. + + extension radius + param environment string + + resource myApplication 'Applications.Core/applications@2023-10-01-preview' = { ... } + + resource database 'Radius.Data/mySqlDatabases@2025-08-01-preview' = { + name: 'database' + properties: { + application: myApplication.id + environment: environment + } + } + + To connect your container to the database, create a connection from the Container resource to the database as shown below. + + resource frontend 'Applications.Core/containers@2023-10-01-preview' = { + name: 'frontend' + properties: { + application: myApplication.id + environment: environment + container: { + image: 'frontend:1.25' + ports: { + web: { + containerPort: 8080 + } + } + } + connections: { + mysqldb: { + source: database.id + } + } + } + } + + The connection automatically injects environment variables into the container for all properties from the database. The environment variables are named `CONNECTION__`. In this example, the connection name is `mysqldb` so the environment variables will be: + + CONNECTION_MYSQLDB_DATABASE + CONNECTION_MYSQLDB_USERNAME + CONNECTION_MYSQLDB_PASSWORD + CONNECTION_MYSQLDB_VERSION + CONNECTION_MYSQLDB_HOST + CONNECTION_MYSQLDB_PORT + + apiVersions: + '2025-08-01-preview': + schema: + type: object + 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." + database: + type: string + description: "(Optional) The name of the database." + username: + type: string + description: "(Optional) The username for connecting to the database." + version: + type: string + enum: ['5.7', '8.0', '8.4'] + description: "(Optional) The major MySQL server version in the X.Y format. Assumed to be 8.4 if not specified." + password: + type: string + description: "(Read-only) The password for connecting to the database." + readOnly: true + host: + type: string + description: "(Read-only) The host name used to connect to the database." + readOnly: true + port: + type: integer + description: "(Read-only) The port number used to connect to the the database." + readOnly: true + required: + - environment \ No newline at end of file diff --git a/Data/mySqlDatabases/recipes/kubernetes/bicep/kubernetes-mysql.bicep b/Data/mySqlDatabases/recipes/kubernetes/bicep/kubernetes-mysql.bicep new file mode 100644 index 00000000..684b8e1c --- /dev/null +++ b/Data/mySqlDatabases/recipes/kubernetes/bicep/kubernetes-mysql.bicep @@ -0,0 +1,130 @@ +@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 + +extension kubernetes with { + kubeConfig: '' + namespace: context.runtime.kubernetes.namespace +} as kubernetes + +@description('Name of the MySQL database. Defaults to the application name.') +param database string = context.resource.properties.?database ?? '${context.application.name}' + +@description('MySQL username. Defaults to -user') +param username string = context.resource.properties.?username ?? '${context.application.name}-user' + +@description('The major MySQL server version in the X.Y format. Defaults to the version 8.4 if not provided.') +@allowed([ + '5.7' + '8.0' + '8.4' +]) +param version string = context.resource.properties.?version ?? '8.4' + +@description('Unique name for the MySQL deployment and service.') +var uniqueName = 'mysql-${uniqueString(context.resource.id)}' + +@description('The MySQL container image to use.') +var mySqlImage = 'mysql:${version}' + +@description('The port the MySQL server listens on.') +var port = 3306 + +@description('MySQL user password. Defaults to a unique generated value.') +var password string = uniqueString(context.resource.id, guid(uniqueName, username)) + +@description('MySQL server root password.') +var root_password string = uniqueString(context.resource.id, guid(uniqueName, 'root')) + +resource mySql 'apps/Deployment@v1' = { + metadata: { + name: uniqueName + } + spec: { + selector: { + matchLabels: { + app: context.application.name + resource: context.resource.name + } + } + template: { + metadata: { + labels: { + app: context.application.name + resource: context.resource.name + 'radapp.io/application': context.application == null ? '' : context.application.name + } + } + spec: { + containers: [ + { + name: 'mysql' + image: mySqlImage + ports: [ + { + containerPort: port + } + ] + env: [ + { + name: 'MYSQL_ROOT_PASSWORD' + value: root_password + } + { + name: 'MYSQL_USER' + value: username + } + { + name: 'MYSQL_PASSWORD' + value: password + } + { + name: 'MYSQL_DATABASE' + value: database + } + ] + } + ] + } + } + } +} + +resource svc 'core/Service@v1' = { + metadata: { + name: uniqueName + labels: { + app: context.application.name + resource: context.resource.name + 'radapp.io/application': context.application == null ? '' : context.application.name + } + } + spec: { + type: 'ClusterIP' + selector: { + app: context.application.name + resource: context.resource.name + } + ports: [ + { + port: port + } + ] + } +} + +output result object = { + resources: [ + '/planes/kubernetes/local/namespaces/${svc.metadata.namespace}/providers/core/Service/${svc.metadata.name}' + '/planes/kubernetes/local/namespaces/${mySql.metadata.namespace}/providers/apps/Deployment/${mySql.metadata.name}' + ] + values: { + host: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local' + port: port + database: database + username: username + } + secrets: { + #disable-next-line outputs-should-not-contain-secrets + password: password + } +} diff --git a/Data/neo4jDatabases/README.md b/Data/neo4jDatabases/README.md new file mode 100644 index 00000000..eaf192a2 --- /dev/null +++ b/Data/neo4jDatabases/README.md @@ -0,0 +1,28 @@ +# Radius.Data/neo4jDatabases + +## Overview + +The `Radius.Data/neo4jDatabases` Resource Type represents a Neo4j graph database. It is intended for application-centric usage and can also be provisioned as a shared resource in a Radius Environment. + +## Recipes + +| Platform | IaC | Recipe Name | Stage | +|------------|-----------|--------------------------------------|-------| +| Kubernetes | Bicep | `kubernetes-neo4j.bicep` | Alpha | + +## Recipe Input Properties + +This Resource Type is in Alpha and does not expose developer-set input properties for the Recipe at this time. In future iterations, additional properties (for example, size and Neo4j-specific configuration) may be introduced. + +## Recipe Output Properties + +Recipes must populate the following read-only properties on the resource: + +- `host` (string): DNS hostname clients use to connect. +- `port` (integer): Bolt port (typically `7687`). +- `username` (string): Username for client connections. +- `password` (string): Password for client connections. + +## Notes + +- The reference Kubernetes recipe is designed for development and evaluation. For production use, consider adding persistence (PVC), authentication, and backup/restore to your own recipe variant. diff --git a/Data/neo4jDatabases/neo4jDatabases.yaml b/Data/neo4jDatabases/neo4jDatabases.yaml new file mode 100644 index 00000000..9f7404e1 --- /dev/null +++ b/Data/neo4jDatabases/neo4jDatabases.yaml @@ -0,0 +1,75 @@ +namespace: Radius.Data +types: + neo4jDatabases: + description: | + The Radius.Data/neo4jDatabases Resource Type deploys a Neo4j graph database. To create a new Neo4j database, define an Application and Container resource in your application definition Bicep file. + + extension radius + param environment string + + resource myapplication 'Radius.Core/applications@2025-08-01-preview' = { ... } + resource myContainer 'Radius.Compute/containers@2025-08-01-preview' = { ... } + + Then add a `neo4jDatabases` resource. + + resource neo4j 'Radius.Data/neo4jDatabases@2025-09-11-preview' = { + name: 'neo4j' + properties: { + application: myapplication.id + environment: environment + } + } + + To connect to the new Neo4j database, add a connection between the container and the new Neo4j database. The final Container resource should look similar to: + + resource myContainer 'Radius.Compute/containers@2025-08-01-preview' = { + name: 'myContainer' + properties: { + environment: environment + application: myapplication.id + containers: { ... } + connections: { + neo4jDb: { + source: neo4j.id + } + } + } + } + + Within your container, your application code can obtain the host, port, username, and password values via environment variables automatically created. + + CONNECTION_NEO4JDB_HOST + CONNECTION_NEO4JDB_PORT + CONNECTION_NEO4JDB_DATABASE + CONNECTION_NEO4JDB_USERNAME + CONNECTION_NEO4JDB_PASSWORD + + apiVersions: + '2025-09-11-preview': + schema: + type: object + properties: + environment: + type: string + description: (Required) The Radius Environment ID. Typically provided by the rad CLI. The typical value is `environment`. + application: + type: string + description: (Optional) The Radius Application ID when the database is app-scoped. Omit to provision a shared database in an Environment. + host: + type: string + readOnly: true + description: (Read-only) The DNS hostname used by clients to connect to Neo4j. + port: + type: integer + readOnly: true + description: (Read-only) The Bolt protocol port used to connect to Neo4j (typically `7687`). + username: + type: string + readOnly: true + description: (Read-only) The username for connecting to Neo4j. + password: + type: string + readOnly: true + description: (Read-only) The password for connecting to Neo4j. + required: + - environment diff --git a/Data/neo4jDatabases/recipes/kubernetes/README.md b/Data/neo4jDatabases/recipes/kubernetes/README.md new file mode 100644 index 00000000..5fdd986f --- /dev/null +++ b/Data/neo4jDatabases/recipes/kubernetes/README.md @@ -0,0 +1,31 @@ +# Kubernetes Recipe - Neo4j (Alpha) + +This Alpha recipe deploys Neo4j to Kubernetes as an `apps/StatefulSet` with a `ReadWriteOnce` PersistentVolumeClaim and a `ClusterIP` Service exposing the Bolt port (7687). It is suitable for local development and evaluation. + +Authentication is enabled via parameters. The recipe accepts a username and password and returns these in the outputs so that the corresponding resource properties can be populated in Radius. + +Outputs: + +- `values.host`: Internal DNS name of the Service +- `values.port`: Bolt port (7687) +- `values.username`: Username provided to the recipe +- `values.database`: Database name used by the deployment +- `secrets.password`: Password provided to the recipe + +## Usage + +This recipe is intended to be registered to a Radius Environment and mapped to `Radius.Data/neo4jDatabases@2025-09-11-preview`. + +When a developer defines a `neo4jDatabases` resource, Radius will invoke this recipe and populate the resource outputs. + +### Parameters + +- `database` (string, default: resource name): Database name to configure. +- `user` (string, default: `neo4j`): Username to provision. +- `password` (secure string, default: `uniqueString(context.resource.id)`): Password to provision. +- `tag` (string, default: `community`): Tag for the `neo4j` container image. + +### Notes + +- This reference recipe enables persistence via a 10Gi PVC and uses a single replica StatefulSet. +- For production use, consider customizing storage class, resource requests/limits, authentication hardening, backup/restore, and service exposure. diff --git a/Data/neo4jDatabases/recipes/kubernetes/bicep/kubernetes-neo4j.bicep b/Data/neo4jDatabases/recipes/kubernetes/bicep/kubernetes-neo4j.bicep new file mode 100644 index 00000000..c93261ef --- /dev/null +++ b/Data/neo4jDatabases/recipes/kubernetes/bicep/kubernetes-neo4j.bicep @@ -0,0 +1,127 @@ +@description('Information about what resource is calling this Recipe. Generated by Radius.') +param context object + +@description('Name of the Neo4j database. Defaults to the name of the Radius resource.') +param database string = context.resource.name + +@description('Neo4j username') +param user string = 'neo4j' + +@description('Neo4j password') +@secure() +#disable-next-line secure-parameter-default +param password string = uniqueString(context.resource.id) + +@description('Tag to pull for the neo4j container image.') +param tag string = 'community' + +extension kubernetes with { + kubeConfig: '' + namespace: context.runtime.kubernetes.namespace +} as kubernetes + +var uniqueName = 'neo4j-${uniqueString(context.resource.id)}' +var port = 7687 + +resource svc 'core/Service@v1' = { + metadata: { + name: uniqueName + labels: { + name: uniqueName + } + } + spec: { + type: 'ClusterIP' + selector: { + app: 'neo4j' + resource: context.resource.name + } + ports: [ + { + port: port + } + ] + } +} + +resource neo4j 'apps/StatefulSet@v1' = { + metadata: { + name: uniqueName + } + spec: { + serviceName: uniqueName + replicas: 1 + selector: { + matchLabels: { + app: 'neo4j' + resource: context.resource.name + } + } + template: { + metadata: { + labels: { + app: 'neo4j' + resource: context.resource.name + // Label pods with the application name so `rad run` can find the logs. + 'radapp.io/application': context.application == null ? '' : context.application.name + } + } + spec: { + containers: [ + { + name: 'neo4j' + image: 'neo4j:${tag}' + ports: [ + { + containerPort: 7474 + name: 'http' + } + { + containerPort: port + name: 'bolt' + } + ] + volumeMounts: [ + { + name: 'neo4j-data' + mountPath: '/data' + } + ] + } + ] + } + } + volumeClaimTemplates: [ + { + metadata: { + name: 'neo4j-data' + } + spec: { + accessModes: ['ReadWriteOnce'] + resources: { + requests: { + storage: '10Gi' + } + } + } + } + ] + } +} + +output result object = { + resources: [ + '/planes/kubernetes/local/namespaces/${svc.metadata.namespace}/providers/core/Service/${svc.metadata.name}' + '/planes/kubernetes/local/namespaces/${neo4j.metadata.namespace}/providers/apps/Deployment/${neo4j.metadata.name}' + ] + values: { + host: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local' + port: port + database: database + username: user + } + secrets: { + #disable-next-line outputs-should-not-contain-secrets + password: password + } +} diff --git a/Makefile b/Makefile index bfb2329e..fce1fd09 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,25 @@ # limitations under the License. # ------------------------------------------------------------ +# Makefile for Radius Resource Types and Recipes Testing +# +# This Makefile provides standardized commands for testing resource types +# locally and in CI/CD pipelines. It supports Kubernetes recipe testing +# with automated setup, validation, and cleanup. +# +# Quick Start: +# make help # Show all available targets +# make install-radius # Set up local test environment +# make test-bicep-recipes # Test Kubernetes Bicep recipes +# +# Common Workflow: +# make install-radius VERSION=edge +# make create-workspace +# make create-resource-types +# make test-bicep-recipes + SHELL := /bin/bash ARROW := \033[34;1m=>\033[0m # order matters for these -include build/help.mk build/validation.mk \ No newline at end of file +include ./.github/build/help.mk ./.github/build/validation.mk \ No newline at end of file diff --git a/README.md b/README.md index 9a4d135a..113f029c 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ Community members can contribute new Resource Types, Recipes, and Recipe Packs t - Contributing Recipe Packs: Coming soon! - [Submitting Issues](docs/contributing/contributing-issues.md): This guide provides an overview of how to submit issues related to Resource Types or Recipes. +**Thanks to everyone who has contributed!** + + + + + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/Security/secrets/app.bicep b/Security/secrets/test/app.bicep similarity index 100% rename from Security/secrets/app.bicep rename to Security/secrets/test/app.bicep diff --git a/bicepconfig.json b/bicepconfig.json deleted file mode 100644 index 49993a0f..00000000 --- a/bicepconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extensions": { - "radius": "br:biceptypes.azurecr.io/radius:latest", - "aws": "br:biceptypes.azurecr.io/aws:latest" - } -} \ No newline at end of file diff --git a/docs/contributing/contributing-resource-types-recipes.md b/docs/contributing/contributing-resource-types-recipes.md index c85685a3..84ff59d3 100644 --- a/docs/contributing/contributing-resource-types-recipes.md +++ b/docs/contributing/contributing-resource-types-recipes.md @@ -515,4 +515,8 @@ output "result" { - Recipes should handle different size/configuration options, allowing users to choose the appropriate configuration for their needs. - Provide outputs required to connect to the resource provisioned by the Recipe. - Use core Radius Resource Types like `containers`, `gateway` and `secrets` where applicable to ensure consistency and reusability. -- Use comments to explain complex logic or important decisions in your Recipe code. \ No newline at end of file +- Use comments to explain complex logic or important decisions in your Recipe code. + +## Integration with CI/CD Testing for Stable Resource Types + +For resource types categorized in the "Stable" maturity level, automated test coverage is required in the repository's CI/CD pipeline. The contribution guide with the steps to follow to update the automated tests can be found in [Contributing Tests for Stable Resource Types](docs/contributing/contributing-resource-types-tests.md) \ No newline at end of file diff --git a/docs/contributing/contributing-resource-types-tests.md b/docs/contributing/contributing-resource-types-tests.md new file mode 100644 index 00000000..a84d1a1b --- /dev/null +++ b/docs/contributing/contributing-resource-types-tests.md @@ -0,0 +1,71 @@ +# Contributing Tests for Stable Resource Types + +Resource Types at the Stable maturity level are required to integrate with Radius CI/CD testing. The test files are discussed below and are only relevant if you are adding test coverage for stable Resource Types. The workflow will run on your PR to validate that the Resource Type definition and Recipes are able to be created with Radius and deployed. + +### `.github/workflows` and `.github/scripts` + +This folder contains the automated testing workflows and scripts. The workflows validate Resource Type definitions, test Recipe deployments, and ensure compatibility with Radius. Scripts provide utility functions for manifest generation, resource verification, and test execution. + +### `.github/build` + +The `build` folder includes logic used to define the make targets. The `help.mk` file provides help documentation for available targets, while `validation.mk` contains all the core testing logic including Radius installation, Resource Type creation, Recipe publishing, and test execution workflows. The `tf-module-server` folder contains a container that is used to host a local module server for Terraform Recipes to referenced during testing. + +### Makefile + +The Makefile provides standardized commands for testing Resource Types locally and in CI/CD. It includes targets for installing dependencies, creating resources, publishing Recipes, running tests, and cleaning up Environments. These targets can be run locally to help with manual testing. + +## Add test coverage for stable Resource Types +These are the steps to follow to ensure that a stable Resource Type is fully integrated with Radius testing in the CI/CD pipelines. + +### Pre-requisites + +1. [**Resource Type Definition**](../contributing/contributing-resource-types-tests.md#resource-type-definition): Defines the structure and properties of your Resource Type +2. [**Recipes**](../contributing/contributing-resource-types-tests.md#recipes-for-the-resource-type): Terraform or Bicep templates for deploying the Resource Type on different platforms + +### Add an app.bicep + +### Add an app.bicep + +1. Create a new `test` folder in your Resource Type root folder. For example, for a Secrets Resource Type, the directory structure would be `/Security/secrets/test`. + +2. Create a application definition Bicep file called `app.bicep` in the test folder. Add an Application resource and a resource for your new Resource Type. Make sure to include the proper extensions for `radius` and your Resource Type. The naming of extension should be the same as your Resource Type. For example, the extension name for the `Radius.Security/secrets` Resource Type should be `secrets`. An `environment` parameter is also needed and will be set by the workflow during automated testing. + +Using the Secrets example, the full application definition should look similar to: + +``` +extension radius +extension secrets + +param environment string + +resource testapp 'Applications.Core/applications@2023-10-01-preview' = { + name: 'testapp' + properties: { + environment: environment + } +} + +resource secret 'Radius.Security/secrets@2025-08-01-preview' = { + name: 'secret' + properties: { + environment: environment + application: app.id + data: { + apikey: { + value: 'abc123xyz' + } + } + } +} +``` + +3. In `validate-common.sh`, update `setup_config()` to contain your new Resource Type. For example, if you were to add a `Radius.Compute/containers` Resource Type, the updated `setup_config()` should look like: +``` +setup_config() { + resource_folders=("Security" "Compute") + declare -g -A folder_to_namespace=( + ["Security"]="Radius.Security" + ["Compute"]="Radius.Compute" + ) +} +``` \ No newline at end of file diff --git a/docs/contributing/submitting-contribution.md b/docs/contributing/submitting-contribution.md index 4c9c8fac..86085e33 100644 --- a/docs/contributing/submitting-contribution.md +++ b/docs/contributing/submitting-contribution.md @@ -2,17 +2,23 @@ Before submitting your contribution. Make sure to check the following: -- [ ] Resource type schema follows naming conventions -- [ ] All properties have clear descriptions -- [ ] Required properties are properly marked -- [ ] Read-only properties are marked as `readOnly: true` +- [ ] File names follow naming conventions and folder structure +- [ ] Platform engineer documentation is in README.md +- [ ] Developer documentation is the top-level description property +- [ ] Example of defining the Resource Type is in the developer documentation +- [ ] Example of using the Resource Type with a Container is in the developer documentation +- [ ] Verified the output of `rad resource-type show` is correct +- [ ] All properties in the Resource Type definition have clear descriptions +- [ ] Enum properties have values defined in `enum: []` +- [ ] Required properties are listed in `required: []` for every object property (not just the top-level properties) +- [ ] Properties about the deployed resource, such as connection strings, are defined as read-only properties and are marked as `readOnly: true` +- [ ] Recipes include a results output variable with all read-only properties set +- [ ] Environment-specific parameters, such as a vnet ID, are exposed for platform engineers to set in the Environment +- [ ] Recipes use the [Recipe context object](https://docs.radapp.io/reference/context-schema/) when possible - [ ] Recipes are provided for at least one platform - [ ] Recipes handle secrets securely -- [ ] Recipes include necessary parameters and outputs -- [ ] Recipes are idempotent and can be run multiple times without issues -- [ ] Recipes output necessary connection information -- [ ] Test your resource type and recipes locally -- [ ] Documentation is complete and clear +- [ ] Recipes are idempotent +- [ ] Resource types and recipes were tested ## Submission Process