diff --git a/.github/workflows/sample-apps.yaml b/.github/workflows/sample-apps.yaml index f0c4b21..c29c117 100644 --- a/.github/workflows/sample-apps.yaml +++ b/.github/workflows/sample-apps.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - app: [cpu-load-gen, hello-world, outbound-http, variabletester] + app: [cpu-load-gen, hello-world, outbound-http, variabletester, redis-sample] env: IMAGE_NAME: ${{ github.repository }} diff --git a/apps/redis-sample/README.md b/apps/redis-sample/README.md new file mode 100644 index 0000000..c046a28 --- /dev/null +++ b/apps/redis-sample/README.md @@ -0,0 +1,90 @@ +# Overview + +This is an OCI-compliant package that can be used to demonstrate how a Spin app interacts with Redis in a Kubernetes cluster. + +# Usage + +## Deploying the Spin app + +Create a Kubernetes manifest file named `redis_client.yaml` with the following code: + +```yaml +apiVersion: core.spinoperator.dev/v1alpha1 +kind: SpinApp +metadata: + name: redis-spinapp +spec: + image: "ghcr.io/spinkube/redis-sample" + replicas: 1 + executor: containerd-shim-spin + variables: + - name: redis_endpoint + value: redis://redis.default.svc.cluster.local:6379 +``` + +Once created, run `kubectl apply -f redis_client.yaml`. + +## Deploying Redis + +Create a Kubernetes manifest file named `redis_db.yaml` with the following code: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis + ports: + - containerPort: 6379 + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 +``` + +Once created, run `kubectl apply -f redis_db.yaml`. + +## Interacting with the Spin app + +In your terminal run `kubectl port-forward svc/redis-spinapp 3000:80`, then in a different terminal window, try the below commands: + +### Place a key-value pair in Redis + +```bash +curl --request PUT --data-binary "Hello, world\!" -H 'x-key: helloKey' localhost:3000 +``` + +### Retrieve a value from Redis + +```bash +curl -H 'x-key: helloKey' localhost:3000 +``` + +### Delete a value from Redis + +```bash +curl --request DELETE -H 'x-key: helloKey' localhost:3000 +``` diff --git a/apps/redis-sample/go.mod b/apps/redis-sample/go.mod new file mode 100644 index 0000000..c3ef841 --- /dev/null +++ b/apps/redis-sample/go.mod @@ -0,0 +1,7 @@ +module github.com/spin_redis_sample + +go 1.20 + +require github.com/fermyon/spin/sdk/go/v2 v2.2.0 + +require github.com/julienschmidt/httprouter v1.3.0 // indirect diff --git a/apps/redis-sample/go.sum b/apps/redis-sample/go.sum new file mode 100644 index 0000000..c283acc --- /dev/null +++ b/apps/redis-sample/go.sum @@ -0,0 +1,4 @@ +github.com/fermyon/spin/sdk/go/v2 v2.2.0 h1:zHZdIqjbUwyxiwdygHItnM+vUUNSZ3CX43jbIUemBI4= +github.com/fermyon/spin/sdk/go/v2 v2.2.0/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/apps/redis-sample/main.go b/apps/redis-sample/main.go new file mode 100644 index 0000000..040f023 --- /dev/null +++ b/apps/redis-sample/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "io" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/redis" + "github.com/fermyon/spin/sdk/go/v2/variables" +) + +var rdb *redis.Client + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + redisEndpoint, err := variables.Get("redis_endpoint") + if err != nil { + http.Error(w, "unable to parse variable 'redis_endpoint'", http.StatusInternalServerError) + return + } + + if redisEndpoint == "" { + http.Error(w, "cannot find 'redis_endpoint' environment variable", http.StatusInternalServerError) + return + } + + rdb = redis.NewClient(redisEndpoint) + + reqKey := r.Header.Get("x-key") + if reqKey == "" { + http.Error(w, "you must include the 'x-key' header in your request", http.StatusBadRequest) + return + } + + if r.Method == "GET" { + value, err := rdb.Get(reqKey) + if err != nil { + http.Error(w, fmt.Sprintf("no value found for key '%s'", reqKey), http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(value) + return + + } else if r.Method == "PUT" { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, fmt.Sprintf("error reading request body: %w", err), http.StatusInternalServerError) + } + defer r.Body.Close() + + if err := rdb.Set(reqKey, bodyBytes); err != nil { + http.Error(w, fmt.Sprintf("unable to add value for key '%s' to database: %w", reqKey, err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + return + + } else if r.Method == "DELETE" { + _, err := rdb.Del(reqKey) + if err != nil { + http.Error(w, fmt.Sprintf("error deleting value for key '%w'", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + return + + } else { + http.Error(w, fmt.Sprintf("method %q is not supported, so please try again using 'GET' or 'PUT' for the HTTP method", r.Method), http.StatusBadRequest) + return + } + }) +} + +func main() {} diff --git a/apps/redis-sample/spin.toml b/apps/redis-sample/spin.toml new file mode 100644 index 0000000..3765fb3 --- /dev/null +++ b/apps/redis-sample/spin.toml @@ -0,0 +1,24 @@ +spin_manifest_version = 2 + +[application] +name = "spin-redis-sample" +version = "0.1.0" +authors = ["Fermyon Engineering Team "] + +[variables] +redis_endpoint = {required = true} + +[[trigger.http]] +route = "/..." +component = "spin-redis-sample" + +[component.spin-redis-sample] +source = "main.wasm" +allowed_outbound_hosts = ["redis://*"] + +[component.spin-redis-sample.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +watch = ["**/*.go", "go.mod"] + +[component.spin-redis-sample.variables] +redis_endpoint = "{{ redis_endpoint }}"