diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edc704f7..3cb4ec04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,7 @@ jobs: kotlin-hello-world-lambda-cdk.zip typescript-hello-world.zip + typescript-hello-world-kubernetes.zip typescript-hello-world-bun.zip typescript-hello-world-cloudflare-worker.zip typescript-hello-world-deno.zip @@ -73,6 +74,7 @@ jobs: typescript-nextjs-fullstack.zip go-hello-world.zip + go-hello-world-kubernetes.zip go-hello-world-lambda-cdk.zip go-tour-of-orchestration.zip go-tour-of-workflows.zip @@ -162,4 +164,4 @@ jobs: secrets: inherit with: source-directory: go/templates/go - destination-repo: go-template \ No newline at end of file + destination-repo: go-template diff --git a/.tools/prepare_release_zip.sh b/.tools/prepare_release_zip.sh index 33a928f2..48fb3101 100755 --- a/.tools/prepare_release_zip.sh +++ b/.tools/prepare_release_zip.sh @@ -53,6 +53,7 @@ create_release_zip typescript/end-to-end-applications/chat-bot typescript-chat-b create_release_zip typescript/end-to-end-applications/ai-image-workflows typescript-ai-image-workflows create_release_zip typescript/patterns-use-cases typescript-patterns-use-cases create_release_zip typescript/templates/node typescript-hello-world +create_release_zip typescript/templates/node-kubernetes typescript-hello-world-kubernetes create_release_zip typescript/templates/lambda typescript-hello-world-lambda create_release_zip typescript/templates/bun typescript-hello-world-bun create_release_zip typescript/templates/cloudflare-worker typescript-hello-world-cloudflare-worker @@ -69,6 +70,7 @@ create_release_zip go/integrations/go-lambda-cdk go-hello-world-lambda-cdk create_release_zip go/integrations/knative-go go-knative-go create_release_zip go/patterns-use-cases go-patterns-use-cases create_release_zip go/templates/go go-hello-world +create_release_zip go/templates/go-kubernetes go-hello-world-kubernetes create_release_zip go/tutorials/tour-of-orchestration-go go-tour-of-orchestration create_release_zip go/tutorials/tour-of-workflows-go go-tour-of-workflows diff --git a/.tools/run_go_tests.sh b/.tools/run_go_tests.sh index 4d14fd98..3b8fd70b 100755 --- a/.tools/run_go_tests.sh +++ b/.tools/run_go_tests.sh @@ -14,6 +14,7 @@ function go_build_check() { go_build_check $PROJECT_ROOT/go/basics go_build_check $PROJECT_ROOT/go/templates/go +go_build_check $PROJECT_ROOT/go/templates/go-kubernetes go_build_check $PROJECT_ROOT/go/integrations/knative-go go_build_check $PROJECT_ROOT/go/integrations/go-lambda-cdk/lambda go_build_check $PROJECT_ROOT/go/tutorials/tour-of-orchestration-go diff --git a/.tools/run_node_tests.sh b/.tools/run_node_tests.sh index 2d26a6a7..a2b0fd6b 100755 --- a/.tools/run_node_tests.sh +++ b/.tools/run_node_tests.sh @@ -12,6 +12,7 @@ function npm_install_check() { npm_install_check $PROJECT_ROOT/typescript/basics npm_install_check $PROJECT_ROOT/typescript/templates/node +npm_install_check $PROJECT_ROOT/typescript/templates/node-kubernetes npm_install_check $PROJECT_ROOT/typescript/templates/lambda npm_install_check $PROJECT_ROOT/typescript/templates/cloudflare-worker npm_install_check $PROJECT_ROOT/typescript/templates/vercel diff --git a/.tools/update_go_examples.sh b/.tools/update_go_examples.sh index 7b2df205..cbd72a43 100755 --- a/.tools/update_go_examples.sh +++ b/.tools/update_go_examples.sh @@ -28,6 +28,7 @@ function bump_go_sdk() { bump_go_sdk $PROJECT_ROOT/go/basics bump_go_sdk $PROJECT_ROOT/go/templates/go +bump_go_sdk $PROJECT_ROOT/go/templates/go-kubernetes bump_go_sdk $PROJECT_ROOT/go/integrations/knative-go bump_go_sdk $PROJECT_ROOT/go/integrations/go-lambda-cdk/lambda bump_go_sdk $PROJECT_ROOT/go/tutorials/tour-of-orchestration-go diff --git a/.tools/update_node_examples.sh b/.tools/update_node_examples.sh index b7c6fd6d..d6a323fe 100755 --- a/.tools/update_node_examples.sh +++ b/.tools/update_node_examples.sh @@ -47,6 +47,7 @@ function bump_restate_sdk_deps() { # Update all projects with package.json bump_restate_sdk_deps $PROJECT_ROOT/typescript/basics bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/node +bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/node-kubernetes bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/lambda bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/typescript-testing bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/bun diff --git a/go/README.md b/go/README.md index e7ce7243..17ada071 100644 --- a/go/README.md +++ b/go/README.md @@ -45,6 +45,7 @@ Examples integrating Restate with other tools and frameworks: Starter templates for new projects: - **[Go Template](templates/go)** [](templates/go/greeter.go) +- **[Go Kubernetes Template](templates/go-kubernetes)**: Deploy to Kubernetes with an in-process tunnel and Kubernetes YAML files. ## Tutorials diff --git a/go/templates/go-kubernetes/.agents/plugins/marketplace.json b/go/templates/go-kubernetes/.agents/plugins/marketplace.json new file mode 100644 index 00000000..b9c8574c --- /dev/null +++ b/go/templates/go-kubernetes/.agents/plugins/marketplace.json @@ -0,0 +1,22 @@ +{ + "name": "restatedev-plugin", + "interface": { + "displayName": "Restate" + }, + "plugins": [ + { + "name": "restatedev", + "source": { + "source": "git-subdir", + "url": "https://github.com/restatedev/skills.git", + "path": "./plugins/restatedev", + "ref": "main" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" + } + ] +} diff --git a/go/templates/go-kubernetes/.claude/settings.json b/go/templates/go-kubernetes/.claude/settings.json new file mode 100644 index 00000000..1c69c4f5 --- /dev/null +++ b/go/templates/go-kubernetes/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "restatedev-plugin": { + "source": { + "source": "github", + "repo": "restatedev/skills" + } + } + }, + "enabledPlugins": { + "restatedev@restatedev-plugin": true + } +} diff --git a/go/templates/go-kubernetes/.dockerignore b/go/templates/go-kubernetes/.dockerignore new file mode 100644 index 00000000..60efd53b --- /dev/null +++ b/go/templates/go-kubernetes/.dockerignore @@ -0,0 +1,6 @@ +.git +.idea +.agents +.claude +*.zip +go-kubernetes diff --git a/go/templates/go-kubernetes/.gitignore b/go/templates/go-kubernetes/.gitignore new file mode 100644 index 00000000..3fb70c32 --- /dev/null +++ b/go/templates/go-kubernetes/.gitignore @@ -0,0 +1,5 @@ +go-kubernetes + +# Restate +.restate +restate-data diff --git a/go/templates/go-kubernetes/Dockerfile b/go/templates/go-kubernetes/Dockerfile new file mode 100644 index 00000000..38bafea6 --- /dev/null +++ b/go/templates/go-kubernetes/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.25 AS builder + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 go build -trimpath -o /service . + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=builder /service /service +ENTRYPOINT ["/service"] diff --git a/go/templates/go-kubernetes/README.md b/go/templates/go-kubernetes/README.md new file mode 100644 index 00000000..fbe7c04b --- /dev/null +++ b/go/templates/go-kubernetes/README.md @@ -0,0 +1,16 @@ +# Hello world - Go Kubernetes template + +Sample Restate service using the Go SDK, with an in-process tunnel, a Dockerfile, and Kubernetes YAML files. + +See the [Kubernetes service deployment guide](https://docs.restate.dev/services/deploy/kubernetes) for more information on using this template to deploy to Kubernetes. + +Build the container image with `docker build -t /my-restate-service:0.0.1 .`. +The Restate operator injects the tunnel configuration when using `tunnelMode: in-process`. The authentication token file is mounted on the deployment. + +Invoke `Greeter/Greet` with a JSON object body: `{"name":"World"}`. + +## Using AI coding tools + +If you use Claude Code or Codex, then the Restate plugin will automatically be installed. For Cursor, consult the [skills repo README](https://github.com/restatedev/skills). + +Plugin repo: https://github.com/restatedev/skills/tree/main diff --git a/go/templates/go-kubernetes/go.mod b/go/templates/go-kubernetes/go.mod new file mode 100644 index 00000000..2b98ad77 --- /dev/null +++ b/go/templates/go-kubernetes/go.mod @@ -0,0 +1,26 @@ +module github.com/restatedev/examples/go/templates/go-kubernetes + +go 1.25.0 + +require ( + github.com/restatedev/sdk-go v1.0.3 + github.com/restatedev/sdk-go/x/tunnel v0.1.1 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/invopop/jsonschema v0.14.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/go/templates/go-kubernetes/go.sum b/go/templates/go-kubernetes/go.sum new file mode 100644 index 00000000..cefa47ed --- /dev/null +++ b/go/templates/go-kubernetes/go.sum @@ -0,0 +1,52 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/restatedev/sdk-go v1.0.3 h1:pDXoFot3eQk5T//2MpTTfwXsMtDgHeoh0grweW8kZxE= +github.com/restatedev/sdk-go v1.0.3/go.mod h1:wBPSXBOgBvwTQhLqujDSeW1BA480JXh6ZcIervk7tnA= +github.com/restatedev/sdk-go/x/tunnel v0.1.1 h1:fIuo9BdzKTJ5qFdDHlAEEctu5bLbFN7WJmxCs7omrk8= +github.com/restatedev/sdk-go/x/tunnel v0.1.1/go.mod h1:/KEBmcooRbYmnii0DiIIxC0vsuBuf0N4xBVAr3K+F3Q= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= +go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/templates/go-kubernetes/greeter.go b/go/templates/go-kubernetes/greeter.go new file mode 100644 index 00000000..dfd3fe2c --- /dev/null +++ b/go/templates/go-kubernetes/greeter.go @@ -0,0 +1,45 @@ +package main + +import ( + "time" + + restate "github.com/restatedev/sdk-go" +) + +// Greeter is a struct which represents a Restate service; reflection will turn exported methods into service handlers +type Greeter struct{} + +type Greeting struct { + Name string `json:"name"` +} + +func (Greeter) Greet(ctx restate.Context, greeting Greeting) (string, error) { + name := greeting.Name + // Durably execute a set of steps; resilient against failures + greetingId := restate.UUID(ctx).String() + + if _, err := restate.Run(ctx, + func(ctx restate.RunContext) (restate.Void, error) { + return SendNotification(greetingId, name) + }, + restate.WithName("Notification"), + ); err != nil { + return "", err + } + + if err := restate.Sleep(ctx, 1*time.Second); err != nil { + return "", err + } + + if _, err := restate.Run(ctx, + func(ctx restate.RunContext) (restate.Void, error) { + return SendReminder(greetingId, name) + }, + restate.WithName("Reminder"), + ); err != nil { + return "", err + } + + // Respond to caller + return "You said hi to " + name + "!", nil +} diff --git a/go/templates/go-kubernetes/k8s/cloud-env.yaml b/go/templates/go-kubernetes/k8s/cloud-env.yaml new file mode 100644 index 00000000..0764e7ff --- /dev/null +++ b/go/templates/go-kubernetes/k8s/cloud-env.yaml @@ -0,0 +1,12 @@ +apiVersion: restate.dev/v1beta1 +kind: RestateCloudEnvironment +metadata: + name: my-cloud-env +spec: + environmentId: env_... + signingPublicKey: publickeyv1_... + region: eu + authentication: + secret: + name: my-cloud-environment-secret + key: token \ No newline at end of file diff --git a/go/templates/go-kubernetes/k8s/deployment.yaml b/go/templates/go-kubernetes/k8s/deployment.yaml new file mode 100644 index 00000000..847feae9 --- /dev/null +++ b/go/templates/go-kubernetes/k8s/deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: restate.dev/v1beta1 +kind: RestateDeployment +metadata: + name: service +spec: + replicas: 1 + restate: + register: + cloud: my-cloud-env + tunnelMode: in-process + selector: + matchLabels: + app: service + template: + metadata: + labels: + app: service + spec: + containers: + - name: service + image: /my-restate-service:0.0.1 + env: + - name: RESTATE_INPROC_AUTH_TOKEN_FILE + value: /var/run/secrets/restate.cloud/token + volumeMounts: + - name: restate-cloud-tunnel-auth + mountPath: /var/run/secrets/restate.cloud + readOnly: true + volumes: + - name: restate-cloud-tunnel-auth + secret: + secretName: restate-cloud-tunnel-auth diff --git a/go/templates/go-kubernetes/main.go b/go/templates/go-kubernetes/main.go new file mode 100644 index 00000000..b0c9bff6 --- /dev/null +++ b/go/templates/go-kubernetes/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "log/slog" + "os" + "os/signal" + "syscall" + + restate "github.com/restatedev/sdk-go" + "github.com/restatedev/sdk-go/server" + "github.com/restatedev/sdk-go/x/tunnel" +) + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + srv := server.NewRestate(). + Bind(restate.Reflect(Greeter{})) + + // The Restate operator injects the RESTATE_INPROC_* tunnel configuration. + if err := tunnel.NewTunnel(srv).Start(ctx); err != nil { + slog.Error("application exited unexpectedly", "err", err.Error()) + os.Exit(1) + } +} diff --git a/go/templates/go-kubernetes/utils.go b/go/templates/go-kubernetes/utils.go new file mode 100644 index 00000000..b1086510 --- /dev/null +++ b/go/templates/go-kubernetes/utils.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + restate "github.com/restatedev/sdk-go" + "math/rand" +) + +func SendNotification(greetingId string, name string) (restate.Void, error) { + if rand.Float32() < 0.7 && name == "Alice" { // 70% chance of failure + fmt.Printf("[👻 SIMULATED] Failed to send notification: %s - %s\n", greetingId, name) + return restate.Void{}, fmt.Errorf("[👻 SIMULATED] Failed to send notification: %s - %s", greetingId, name) + } + fmt.Printf("Notification sent: %s - %s\n", greetingId, name) + return restate.Void{}, nil +} + +func SendReminder(greetingId string, name string) (restate.Void, error) { + if rand.Float32() < 0.7 && name == "Alice" { // 70% chance of failure + fmt.Printf("[👻 SIMULATED] Failed to send reminder: %s\n", greetingId) + return restate.Void{}, fmt.Errorf(" [👻 SIMULATED] Failed to send reminder: %s", greetingId) + } + fmt.Printf("Reminder sent: %s\n", greetingId) + return restate.Void{}, nil +} diff --git a/typescript/README.md b/typescript/README.md index 8d5f4373..11a19f23 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -68,6 +68,7 @@ Complete applications built with Restate: Starter templates for new projects: - **[Node Template](templates/node)** +- **[Node.js Kubernetes Template](templates/node-kubernetes)**: Deploy to Kubernetes with an ingress tunnel and Kubernetes YAML files. - **[Bun Template](templates/bun)** - **[CloudFlare Workers Template](templates/cloudflare-worker)** - **[Deno Template](templates/deno)** @@ -79,4 +80,4 @@ Starter templates for new projects: Step-by-step guides to learn Restate: - **[Tour of Orchestration](tutorials/tour-of-orchestration-typescript)**: An introduction to microservice orchestration with Restate as described in the [documentation](https://docs.restate.dev/tour/microservice-orchestration). -- **[Tour of Workflows](tutorials/tour-of-workflows-typescript)**: An introduction to resilient workflows with Restate as described in the [documentation](https://docs.restate.dev/tour/workflows). \ No newline at end of file +- **[Tour of Workflows](tutorials/tour-of-workflows-typescript)**: An introduction to resilient workflows with Restate as described in the [documentation](https://docs.restate.dev/tour/workflows). diff --git a/typescript/templates/node-kubernetes/.agents/plugins/marketplace.json b/typescript/templates/node-kubernetes/.agents/plugins/marketplace.json new file mode 100644 index 00000000..b9c8574c --- /dev/null +++ b/typescript/templates/node-kubernetes/.agents/plugins/marketplace.json @@ -0,0 +1,22 @@ +{ + "name": "restatedev-plugin", + "interface": { + "displayName": "Restate" + }, + "plugins": [ + { + "name": "restatedev", + "source": { + "source": "git-subdir", + "url": "https://github.com/restatedev/skills.git", + "path": "./plugins/restatedev", + "ref": "main" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" + } + ] +} diff --git a/typescript/templates/node-kubernetes/.claude/settings.json b/typescript/templates/node-kubernetes/.claude/settings.json new file mode 100644 index 00000000..1c69c4f5 --- /dev/null +++ b/typescript/templates/node-kubernetes/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "restatedev-plugin": { + "source": { + "source": "github", + "repo": "restatedev/skills" + } + } + }, + "enabledPlugins": { + "restatedev@restatedev-plugin": true + } +} diff --git a/typescript/templates/node-kubernetes/.dockerignore b/typescript/templates/node-kubernetes/.dockerignore new file mode 100644 index 00000000..6a9bc407 --- /dev/null +++ b/typescript/templates/node-kubernetes/.dockerignore @@ -0,0 +1,4 @@ +.* +node_modules +dist +Dockerfile diff --git a/typescript/templates/node-kubernetes/.gitignore b/typescript/templates/node-kubernetes/.gitignore new file mode 100644 index 00000000..b5fa0646 --- /dev/null +++ b/typescript/templates/node-kubernetes/.gitignore @@ -0,0 +1,22 @@ +# Node +node_modules +dist + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Restate +.restate +restate-data \ No newline at end of file diff --git a/typescript/templates/node-kubernetes/Dockerfile b/typescript/templates/node-kubernetes/Dockerfile new file mode 100644 index 00000000..fc680951 --- /dev/null +++ b/typescript/templates/node-kubernetes/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-alpine + +# dumb-init helps handling SIGTERM and SIGINT correctly +RUN apk add dumb-init + +WORKDIR /usr/src/app + +# copy package.json and package-lock.json separately to cache dependencies +COPY package*.json . +RUN npm install + +COPY --chown=node:node . . + +RUN npm run build + +RUN npm prune --production +ENV NODE_ENV=production + +EXPOSE 9080 +USER node +CMD ["dumb-init", "node", "./dist/app.js"] diff --git a/typescript/templates/node-kubernetes/README.md b/typescript/templates/node-kubernetes/README.md new file mode 100644 index 00000000..8ce4c170 --- /dev/null +++ b/typescript/templates/node-kubernetes/README.md @@ -0,0 +1,11 @@ +# Hello world - Node.js Kubernetes template + +Sample Restate service using the TypeScript SDK on Node.js, with an ingress tunnel, a Dockerfile, and Kubernetes YAML files. + +See the [Kubernetes service deployment guide](https://docs.restate.dev/services/deploy/kubernetes) for more information on using this template to deploy to Kubernetes. + +## Using AI coding tools + +If you use Claude Code or Codex, then the Restate plugin will automatically be installed. For Cursor, consult the [skills repo README](https://github.com/restatedev/skills). + +Plugin repo: https://github.com/restatedev/skills/tree/main diff --git a/typescript/templates/node-kubernetes/k8s/cloud-env.yaml b/typescript/templates/node-kubernetes/k8s/cloud-env.yaml new file mode 100644 index 00000000..0764e7ff --- /dev/null +++ b/typescript/templates/node-kubernetes/k8s/cloud-env.yaml @@ -0,0 +1,12 @@ +apiVersion: restate.dev/v1beta1 +kind: RestateCloudEnvironment +metadata: + name: my-cloud-env +spec: + environmentId: env_... + signingPublicKey: publickeyv1_... + region: eu + authentication: + secret: + name: my-cloud-environment-secret + key: token \ No newline at end of file diff --git a/typescript/templates/node-kubernetes/k8s/deployment.yaml b/typescript/templates/node-kubernetes/k8s/deployment.yaml new file mode 100644 index 00000000..847feae9 --- /dev/null +++ b/typescript/templates/node-kubernetes/k8s/deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: restate.dev/v1beta1 +kind: RestateDeployment +metadata: + name: service +spec: + replicas: 1 + restate: + register: + cloud: my-cloud-env + tunnelMode: in-process + selector: + matchLabels: + app: service + template: + metadata: + labels: + app: service + spec: + containers: + - name: service + image: /my-restate-service:0.0.1 + env: + - name: RESTATE_INPROC_AUTH_TOKEN_FILE + value: /var/run/secrets/restate.cloud/token + volumeMounts: + - name: restate-cloud-tunnel-auth + mountPath: /var/run/secrets/restate.cloud + readOnly: true + volumes: + - name: restate-cloud-tunnel-auth + secret: + secretName: restate-cloud-tunnel-auth diff --git a/typescript/templates/node-kubernetes/package.json b/typescript/templates/node-kubernetes/package.json new file mode 100644 index 00000000..7787a7cd --- /dev/null +++ b/typescript/templates/node-kubernetes/package.json @@ -0,0 +1,28 @@ +{ + "name": "restate-ts-template", + "version": "0.0.1", + "description": "Template for JavaScript/TypeScript services running with Restate (https://github.com/restatedev/) ", + "main": "app.js", + "type": "commonjs", + "scripts": { + "build": "tsc --noEmitOnError", + "start": "node ./dist/app.js", + "dev": "tsx watch ./src/app.ts", + "prebundle": "rm -rf dist", + "bundle": "esbuild src/app.ts --bundle --minify --sourcemap --platform=node --target=es2020 --outfile=dist/app.js", + "postbundle": "cd dist && zip -r index.zip app.js*", + "app": "npm run start", + "app-dev": "npm run dev" + }, + "dependencies": { + "@restatedev/restate-sdk": "^1.17.0", + "@restatedev/restate-sdk-tunnel": "^1.17.0", + "zod": "^4.3" + }, + "devDependencies": { + "@types/node": "^22", + "esbuild": "^0.25.4", + "tsx": "^4.19.2", + "typescript": "^5.4.5" + } +} diff --git a/typescript/templates/node-kubernetes/src/app.ts b/typescript/templates/node-kubernetes/src/app.ts new file mode 100644 index 00000000..5e197672 --- /dev/null +++ b/typescript/templates/node-kubernetes/src/app.ts @@ -0,0 +1,34 @@ +import * as restate from "@restatedev/restate-sdk"; +import { sendNotification, sendReminder } from "./utils"; + +import { z } from "zod"; +import { connectTunnel } from "@restatedev/restate-sdk-tunnel"; + +const Greeting = z.object({ + name: z.string().default("World"), +}); + +const GreetingResponse = z.object({ + result: z.string(), +}); + +const greeter = restate.service({ + name: "Greeter", + handlers: { + greet: restate.createServiceHandler( + { input: restate.serde.schema(Greeting), output: restate.serde.schema(GreetingResponse) }, + async (ctx: restate.Context, { name }) => { + // Durably execute a set of steps; resilient against failures + const greetingId = ctx.rand.uuidv4(); + await ctx.run("Notification", () => sendNotification(greetingId, name)); + await ctx.sleep({ seconds: 1 }); + await ctx.run("Reminder", () => sendReminder(greetingId, name)); + + // Respond to caller + return { result: `You said hi to ${name}!` }; + }, + ), + }, +}); + +connectTunnel({ services: [greeter] }); diff --git a/typescript/templates/node-kubernetes/src/utils.ts b/typescript/templates/node-kubernetes/src/utils.ts new file mode 100644 index 00000000..bb5e7599 --- /dev/null +++ b/typescript/templates/node-kubernetes/src/utils.ts @@ -0,0 +1,20 @@ +// You can remove this file. +// It's only purpose is providing stubs for the template. + +export function sendNotification(greetingId: string, name: string) { + if (Math.random() < 0.7 && name == "Alice") { + // 70% chance of failure + console.error(`[👻 SIMULATED] Failed to send notification: ${greetingId} - ${name}`); + throw new Error(`[👻 SIMULATED] Failed to send notification ${greetingId} - ${name}`); + } + console.log(`Notification sent: ${greetingId} - ${name}`); +} + +export function sendReminder(greetingId: string, name: string) { + if (Math.random() < 0.7 && name == "Alice") { + // 70% chance of failure + console.error(`[👻 SIMULATED] Failed to send reminder: ${greetingId}`); + throw new Error(`[👻 SIMULATED] Failed to send reminder: ${greetingId}`); + } + console.log(`Reminder sent: ${greetingId}`); +} diff --git a/typescript/templates/node-kubernetes/tsconfig.json b/typescript/templates/node-kubernetes/tsconfig.json new file mode 100644 index 00000000..f23f9bf1 --- /dev/null +++ b/typescript/templates/node-kubernetes/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["esnext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "nodenext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "skipDefaultLibCheck": true /* Skip type checking .d.ts files that are included with TypeScript. */ + }, + "include": ["src/"] +}