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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ The current repository offers a codespace setup with Radius and its dependencies
| **eshop** | A Rad-ified version of eShop on Containers, the .NET reference app
| **eshop-dapr** | A Rad-ified version of eShop on Dapr
| **volumes** | An app to interact with mounted volumes

### Resource type samples

Each of these samples connects a real third-party application to a cloud resource provisioned through a Radius resource type wired directly to a standard Azure Verified Module (AVM). Every directory is self-contained (`app.bicep`, `env-azure.bicep`, `bicepconfig.json`, and any application source) with its own README.

| Sample | Resource type | Application |
|--------|---------------|-------------|
| **kafka-ui** | `Radius.Messaging/kafka` (Azure Event Hubs) | Kafbat Kafka UI |
| **berriai-litellm** | `Radius.AI/models` (Azure OpenAI) | LiteLLM proxy |
| **mongo-express-mongo-express** | `Radius.Data/mongoDatabases` (Azure Cosmos DB for MongoDB) | mongo-express |
| **dockersamples-todo-list-app** | `Radius.Data/mySqlDatabases` (Azure MySQL) | Docker todo-list-app |
| **sosedoff-pgweb** | `Radius.Data/postgreSqlDatabases` (Azure PostgreSQL) | pgweb, built from source and run |
| **warpstreamlabs-bento** | `Radius.Messaging/rabbitMQ` (Azure Service Bus) | Bento streaming pipeline, built from source |
| **radius-project-samples-demo** | `Radius.Data/redisCaches` (Azure Managed Redis) | Radius samples demo app |
| **sqlpad-sqlpad** | `Radius.Data/sqlServerDatabases` (Azure SQL) | SQLPad SQL client UI, built from source |
| **azure-search-api** | `Radius.AI/search` (Azure AI Search) | Go search API service, built from source |
| **drakkan-sftpgo** | `Radius.Storage/objectStorage` (Azure Blob Storage) | SFTPGo |
| **spring-projects-spring-petclinic** | `Radius.Data/postgreSqlDatabases` (Azure PostgreSQL) | Spring PetClinic, built from source |
| **dotnet-eshop** | `Radius.Data/postgreSqlDatabases` (Azure PostgreSQL) | .NET eShop microservices, built from source |
| **finos-traderx** | `Radius.Data/postgreSqlDatabases` (Azure PostgreSQL) | FINOS TraderX trading platform, built from source |
35 changes: 35 additions & 0 deletions samples/azure-search-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Go search API service + Radius.AI/search (Azure AI Search)

This sample provisions an Azure AI Search service through `Radius.AI/search` using an Azure Verified Module (AVM) recipe. It builds and runs a small Go HTTP API that stores documents in the search index and queries them through the Azure AI Search REST API.

## What this sample shows
- **Resource type:** `Radius.AI/search`
- **Cloud backing (Azure):** Azure AI Search via the AVM `mcr.microsoft.com/bicep/avm/res/search/search-service:0.12.2` module (`env-azure.bicep`).
- **Application:** Go search API service `v1`, built from source via `Radius.Compute/containerImages` and run via `imageReference`.
- **Credential model:** The recipe exposes the admin key under nested `outputs.secrets.apiKey`. The app declares `connections.search` (Radius auto-injects the non-secret `CONNECTION_SEARCH_ENDPOINT`) and consumes the key explicitly as `CONNECTION_SEARCH_APIKEY` via a `secretKeyRef` backed by `searchService.properties.secrets`, which the Go service reads.

## Files
| File | Role |
| --- | --- |
| `app.bicep` | Developer view: the Radius application, search resource, search API image build, and search API container. |
| `env-azure.bicep` | Platform-engineer view: the `recipePacks` binding the type to the AVM module plus supporting recipes, and the `environment`. |
| `bicepconfig.json` | Radius Bicep extension configuration. |
| `src/` | Go source for the search API service (stdlib only), its tests, and Dockerfile. |

## Deploying
Deploy `env-azure.bicep` (the environment) first, then `app.bicep`.

This sample builds its container image from source with a `Radius.Compute/containerImages` recipe and pushes it to a registry you supply. When deploying `env-azure.bicep`, provide:
- `containerImagesRegistry` — an OCI registry you can push to (e.g. `ghcr.io/your-org`).
- `containerImagesRegistrySecretName` — the name of a Kubernetes Secret (`username`/`password`) in the environment namespace, used to authenticate the BuildKit **push** of the built image. Create it before deploying.

Image **pull** auth is separate: the `containerImages` recipe does not configure kubelet pull credentials, so the built image repository must be publicly readable by the cluster, or you must configure a Kubernetes image-pull secret (e.g. a `kubernetes.io/dockerconfigjson` `imagePullSecret` on the namespace's `default` ServiceAccount) before deploying `app.bicep` — otherwise the app pods fail with `ImagePullBackOff`.

## Notes
The containerImages recipe builds from `git::https://github.com/radius-project/samples.git//samples/azure-search-api/src?ref=edge`; this source lands in this repo on merge. The service uses only the Go standard library, calls Azure AI Search REST API version `2024-07-01`, ensures an index on startup, and exposes `GET /healthz`, `POST /documents`, and `GET /search?q=...` on port 8080.

The default index name is `radius-sample`, overridable with `SEARCH_INDEX_NAME`. `POST /documents` accepts JSON documents with `id` and `content`, and `/search` returns the upstream Azure AI Search JSON response.

Additional deployment details:
- The Azure AI Search service uses the Basic SKU with one replica and one partition.
- The container image builds with the Dockerfile under `src/`.
64 changes: 64 additions & 0 deletions samples/azure-search-api/app.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
extension radius

@description('The ID of your Radius Environment. Set automatically by the rad CLI.')
param environment string

resource app 'Radius.Core/applications@2025-08-01-preview' = {
name: 'search-azure-app-test'
properties: {
environment: environment
}
}

resource searchService 'Radius.AI/search@2025-08-01-preview' = {
name: 'search'
properties: {
environment: environment
application: app.id
}
}

resource searchApiImage 'Radius.Compute/containerImages@2025-08-01-preview' = {
name: 'search-api-image'
properties: {
environment: environment
application: app.id
tag: 'v1'
build: {
source: 'git::https://github.com/radius-project/samples.git//samples/azure-search-api/src?ref=edge'
}
}
}

resource searchapictr 'Radius.Compute/containers@2025-08-01-preview' = {
name: 'searchapictr'
properties: {
environment: environment
application: app.id
containers: {
searchapi: {
image: searchApiImage.properties.imageReference
ports: {
web: {
containerPort: 8080
}
}
env: {
CONNECTION_SEARCH_APIKEY: {
valueFrom: {
secretKeyRef: {
secretName: searchService.properties.secrets.name
key: 'apiKey'
}
}
}
}
}
}
connections: {
search: {
source: searchService.id
}
}
}
}
8 changes: 8 additions & 0 deletions samples/azure-search-api/bicepconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"experimentalFeaturesEnabled": {
"extensibility": true
},
"extensions": {
"radius": "br:biceptypes.azurecr.io/radius:latest"
}
}
86 changes: 86 additions & 0 deletions samples/azure-search-api/env-azure.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
extension radius

@description('Azure subscription ID the environment provisions resources into.')
param azureSubscriptionId string

@description('Azure resource group the environment provisions resources into. Must already exist.')
param azureResourceGroup string

@description('Globally-unique Azure AI Search service name (2-60 lowercase letters, digits, or hyphens; cannot start or end with a hyphen). The workflow generates a unique value per run.')
param searchServiceName string

@description('OCI ref for the Radius.Compute/containers recipe. Override to a custom build if needed; defaults to the released public recipe.')
param containersRecipe string = 'ghcr.io/radius-project/kube-recipes/containers:latest'

@description('OCI registry the containerImages recipe builds and pushes the search-api image to (e.g. `ghcr.io/myorg`).')
param containerImagesRegistry string

@description('Kubernetes Secret (in the environment namespace) holding the push registry `username`/`password`.')
param containerImagesRegistrySecretName string

resource recipes 'Radius.Core/recipePacks@2025-08-01-preview' = {
name: 'search-azure-avm'
properties: {
recipes: {
'Radius.AI/search': {
kind: 'bicep'

source: 'mcr.microsoft.com/bicep/avm/res/search/search-service:0.12.2'
parameters: {
name: searchServiceName
sku: 'basic'
disableLocalAuth: false
replicaCount: 1
partitionCount: 1

enableTelemetry: false
}

outputs: {
endpoint: 'endpoint'
secrets: {
apiKey: 'primaryKey'
}
}
}

'Radius.Compute/containers': {
kind: 'bicep'
source: containersRecipe
}

'Radius.Security/secrets': {
kind: 'bicep'
source: 'ghcr.io/radius-project/kube-recipes/secrets:latest'
}

'Radius.Compute/containerImages': {
kind: 'terraform'
source: 'git::https://github.com/radius-project/resource-types-contrib.git//Compute/containerImages/recipes/kubernetes/terraform?ref=b9e0fad536a53349b98f94c5be961db84845e1b7'
parameters: {
registry: containerImagesRegistry
registrySecretName: containerImagesRegistrySecretName
}
}
}
}
}

resource env 'Radius.Core/environments@2025-08-01-preview' = {
name: 'azure'
properties: {
providers: {
azure: {
subscriptionId: azureSubscriptionId
resourceGroupName: azureResourceGroup
}

kubernetes: {
namespace: 'default'
}
}
recipePacks: [
recipes.id
]
}
}
11 changes: 11 additions & 0 deletions samples/azure-search-api/src/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod ./
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/azure-search-api .

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/azure-search-api /azure-search-api
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/azure-search-api"]
3 changes: 3 additions & 0 deletions samples/azure-search-api/src/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/radius-project/samples/azure-search-api

go 1.23
95 changes: 95 additions & 0 deletions samples/azure-search-api/src/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"encoding/json"
"log"
"net/http"
)

type Server struct {
client *SearchClient
logger *log.Logger
}

func NewServer(client *SearchClient, logger *log.Logger) http.Handler {
if logger == nil {
logger = log.Default()
}
server := &Server{client: client, logger: logger}

mux := http.NewServeMux()
mux.HandleFunc("/healthz", server.healthz)
mux.HandleFunc("/documents", server.documents)
mux.HandleFunc("/search", server.search)
return mux
}

func (s *Server) healthz(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
}

func (s *Server) documents(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
defer r.Body.Close()

var doc Document
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&doc); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON document")
return
}
if doc.ID == "" || doc.Content == "" {
writeError(w, http.StatusBadRequest, "id and content are required")
return
}

if err := s.client.UploadDocument(r.Context(), doc); err != nil {
s.logger.Printf("document upload failed: %v", err)
writeError(w, http.StatusBadGateway, "search service request failed")
return
}

w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
}

func (s *Server) search(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}

query := r.URL.Query().Get("q")
if query == "" {
writeError(w, http.StatusBadRequest, "q query parameter is required")
return
}

body, err := s.client.Search(r.Context(), query)
if err != nil {
s.logger.Printf("search failed: %v", err)
writeError(w, http.StatusBadGateway, "search service request failed")
return
}

w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}

func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
48 changes: 48 additions & 0 deletions samples/azure-search-api/src/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"context"
"log"
"net/http"
"os"
"time"
)

const (
defaultPort = "8080"
defaultIndexName = "radius-sample"
)

func main() {
logger := log.New(os.Stdout, "azure-search-api: ", log.LstdFlags)

port := getenvDefault("PORT", defaultPort)
indexName := getenvDefault("SEARCH_INDEX_NAME", defaultIndexName)
client := NewSearchClient(
os.Getenv("CONNECTION_SEARCH_ENDPOINT"),
os.Getenv("CONNECTION_SEARCH_APIKEY"),
indexName,
&http.Client{Timeout: 30 * time.Second},
)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := client.EnsureIndex(ctx); err != nil {
logger.Printf("warning: unable to ensure search index %q at startup: %v", indexName, err)
} else {
logger.Printf("search index %q is ready", indexName)
}
cancel()

addr := ":" + port
logger.Printf("listening on %s", addr)
if err := http.ListenAndServe(addr, NewServer(client, logger)); err != nil {
logger.Fatalf("server stopped: %v", err)
}
}

func getenvDefault(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
Loading