diff --git a/.github/workflows/integration-tests-samples.yml b/.github/workflows/integration-tests-samples.yml
new file mode 100644
index 000000000..31269881b
--- /dev/null
+++ b/.github/workflows/integration-tests-samples.yml
@@ -0,0 +1,62 @@
+name: Integration test for akka-sample-cluster-kubernetes-scala
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+ - release-*
+ tags-ignore: [ v.* ]
+ schedule:
+ - cron: '0 2 * * *' # every day 2am
+
+permissions:
+ contents: read
+
+jobs:
+ integration-test:
+ name: Integration Tests for akka-sample-cluster-kubernetes-scala
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout
+ # https://github.com/actions/checkout/releases
+ # v4.1.1
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+ with:
+ fetch-depth: 0
+
+ - name: Checkout GitHub merge
+ if: github.event.pull_request
+ run: |-
+ git fetch origin pull/${{ github.event.pull_request.number }}/merge:scratch
+ git checkout scratch
+
+ - name: Cache Coursier cache
+ # https://github.com/coursier/cache-action/releases
+ # v6.4.5
+ uses: coursier/cache-action@1ff273bff02a8787bc9f1877d347948af647956d
+
+ - name: Set up JDK 17
+ # https://github.com/coursier/setup-action/releases
+ # v1.3.5
+ uses: coursier/setup-action@7bde40eee928896f074dbb76d22dd772eed5c65f
+ with:
+ jvm: temurin:1.17.0
+
+ - name: Setup Minikube
+ # https://github.com/manusa/actions-setup-minikube/releases
+ # v2.7.1
+ uses: manusa/actions-setup-minikube@4582844dcacbf482729f8d7ef696f515d2141bb9
+ with:
+ minikube version: 'v1.32.0'
+ kubernetes version: 'v1.28.4'
+ driver: docker
+ start args: '--addons=ingress'
+
+ - name: Run Integration Tests
+ run: |-
+ ./integration-test/akka-sample-cluster-kubernetes-scala/test.sh
+
+ - name: Print logs on failure
+ if: ${{ failure() }}
+ run: find . -name "*.log" -exec ./scripts/cat-log.sh {} \;
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1ce4cac5e..07bb83d13 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -76,6 +76,7 @@ jobs:
- name: Publish
run: |-
+ scripts/prepare-downloads.sh
eval "$(ssh-agent -s)"
echo $AKKA_RSYNC_GUSTAV | base64 -d > .github/id_rsa
chmod 600 .github/id_rsa
diff --git a/.gitignore b/.gitignore
index c35fcc96c..cc2ca53ee 100755
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,6 @@ target/
.metals
.vscode
+
+# attachments created by scripts/prepare-downloads.sh
+docs/src/main/paradox/attachments
diff --git a/docs/release-train-issue-template.md b/docs/release-train-issue-template.md
index 6d6030cc5..0e3104417 100644
--- a/docs/release-train-issue-template.md
+++ b/docs/release-train-issue-template.md
@@ -15,6 +15,7 @@ Variables to be expanded in this template:
- [ ] Check that open PRs and issues assigned to the milestone are reasonable
- [ ] Update the Change date and version in the LICENSE file.
+- [ ] Update the Akka Management version in the samples to $VERSION$, otherwise the published zip files of the samples will have the old version.
- [ ] Create a new milestone for the [next version](https://github.com/akka/akka-management/milestones)
- [ ] Close the [$VERSION$ milestone](https://github.com/akka/akka-management/milestones?direction=asc&sort=due_date)
- [ ] Make sure all important PRs have been merged
@@ -46,7 +47,6 @@ For important patch releases, and only if critical issues have been fixed:
- [ ] Send a release notification to [Lightbend discuss](https://discuss.akka.io)
- [ ] Tweet using the [@akkateam](https://twitter.com/akkateam/) account (or ask someone to) about the new release
-- [ ] Announce on [Gitter akka/akka](https://gitter.im/akka/akka)
- [ ] Announce internally (with links to Tweet, discuss)
For minor or major releases:
@@ -56,6 +56,6 @@ For minor or major releases:
### Afterwards
- [ ] Update [akka-dependencies bom](https://github.com/lightbend/akka-dependencies) and version for [Akka module versions](https://doc.akka.io/docs/akka-dependencies/current/) in [akka-dependencies repo](https://github.com/akka/akka-dependencies)
-- [ ] Update [Akka Guide samples](https://github.com/akka/akka-platform-guide)
+- [ ] Update [Akka Guide samples](https://github.com/lightbend/akka-guide)
- Close this issue
diff --git a/docs/src/main/paradox/bootstrap/recipes.md b/docs/src/main/paradox/bootstrap/recipes.md
index a38514096..40aff8b65 100644
--- a/docs/src/main/paradox/bootstrap/recipes.md
+++ b/docs/src/main/paradox/bootstrap/recipes.md
@@ -3,11 +3,6 @@
A set of integration tests projects can be found in [integration-test folder of the Akka Management project](https://github.com/akka/akka-management/tree/main/integration-test).
These test various Akka management features together in various environments such as Kubernetes.
-The following samples exist as standalone projects:
-
-* [Akka Cluster bootstrap using the Kubernetes API with Java/Maven](https://github.com/akka/akka-sample-cluster-kubernetes-java)
-* [Akka Cluster bootstrap using DNS in Kubernetes](https://github.com/akka/akka-sample-cluster-kubernetes-dns-java)
-
## Local
To run Bootstrap locally without any dependencies such as DNS or Kubernetes see the @ref[`local` example](local-config.md)
@@ -25,7 +20,10 @@ The recommended approach is to:
### Example project
-To get started, it might be helpful to have a look at the [Akka Cluster on Kubernetes](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-java) example project.
+To get started, it might be helpful to have a look at the example projects.
+
+* [Akka Cluster bootstrap using the Kubernetes API with Java/Maven](../attachments/akka-sample-cluster-kubernetes-java.zip)
+* [Akka Cluster bootstrap using the Kubernetes API with Scala/sbt](../attachments/akka-sample-cluster-kubernetes-scala.zip)
### Kubernetes Deployment
diff --git a/docs/src/main/paradox/kubernetes-deployment/index.md b/docs/src/main/paradox/kubernetes-deployment/index.md
index d355f0a8a..9c1c4bb12 100644
--- a/docs/src/main/paradox/kubernetes-deployment/index.md
+++ b/docs/src/main/paradox/kubernetes-deployment/index.md
@@ -3,8 +3,8 @@
For this guide we will be using the Akka Cluster in Kubernetes sample.
It is available for both:
- * [Java](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-java)
- * [Scala](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-scala)
+* [Java](../attachments/akka-sample-cluster-kubernetes-java.zip)
+* [Scala](../attachments/akka-sample-cluster-kubernetes-scala.zip)
@@toc { depth=2 }
diff --git a/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md b/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md
index 50338c96d..79d0a8b96 100644
--- a/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md
+++ b/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md
@@ -8,8 +8,8 @@ In preparation for production, we need to do two main things:
The final configuration file and deployment spec are in the sample application.
In this guide we will show snippets. Locations of the samples:
-* [Java](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-java)
-* [Scala](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-scala)
+* [Java](../attachments/akka-sample-cluster-kubernetes-java.zip)
+* [Scala](../attachments/akka-sample-cluster-kubernetes-scala.zip)
## Deployment Spec
diff --git a/integration-test/akka-sample-cluster-kubernetes-scala/test.sh b/integration-test/akka-sample-cluster-kubernetes-scala/test.sh
new file mode 100755
index 000000000..1bd70499d
--- /dev/null
+++ b/integration-test/akka-sample-cluster-kubernetes-scala/test.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+set -exu
+
+export NAMESPACE=appka-1
+export APP_NAME=appka
+export PROJECT_NAME=akka-sample-cluster-kubernetes-scala
+export DEPLOYMENT=samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml
+
+integration-test/scripts/kubernetes-test.sh
+
diff --git a/samples/akka-sample-cluster-kubernetes-java/README.md b/samples/akka-sample-cluster-kubernetes-java/README.md
new file mode 100644
index 000000000..7e896ebc9
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/README.md
@@ -0,0 +1,58 @@
+# akka-sample-cluster-kubernetes-java
+akka sample cluster with kubernetes discovery in scala
+
+This is an example SBT project showing how to create an Akka Cluster on
+Kubernetes.
+
+## Kubernetes Instructions
+
+## Starting
+
+First, package the application and make it available locally as a docker image:
+
+ mvn clean package docker:build
+
+Then `akka-cluster.yml` should be sufficient to deploy a 2-node Akka Cluster, after
+creating a namespace for it:
+
+ kubectl apply -f kubernetes/namespace.json
+ kubectl config set-context --current --namespace=appka-1
+ kubectl apply -f kubernetes/akka-cluster.yml
+
+To check what you have done in Kubernetes so far, you can do:
+
+ kubectl get deployments
+ kubectl get pods
+ kubectl get replicasets
+ kubectl cluster-info dump
+ kubectl logs appka-79c98cf745-abcdee # pod name
+
+Finally, create a service so that you can then test [http://127.0.0.1:8080](http://127.0.0.1:8080)
+for 'hello world':
+
+ kubectl expose deployment appka --type=LoadBalancer --name=appka-service
+ kubectl port-forward svc/appka-service 8080:8080
+
+To wipe everything clean and start over, do:
+
+ kubectl delete namespaces appka-1
+
+## Running in a real Kubernetes cluster
+
+#### Publish to a registry the cluster can access e.g. Dockerhub with the kubakka user
+
+The app image must be in a registry the cluster can see. The build.sbt uses DockerHub by default.
+Use `mvn -Ddocker.registry=$DOCKER_REPO_URL/$NAMESPACE` if your cluster can't access DockerHub.
+
+To push an image to docker hub run:
+
+ mvn -am -pl bootstrap-demo-kubernetes-api package docker:push
+
+And remove the `imagePullPolicy: Never` from the deployments. Then you can use the same `kubectl` commands
+as described in the [Starting](#starting) section.
+
+## How it works
+
+This example uses [Akka Cluster Bootstrap](https://doc.akka.io/docs/akka-management/current/bootstrap/index.html)
+to initialize the cluster, using the [Kubernetes API discovery mechanism](https://doc.akka.io/docs/akka-management/current/discovery/index.html#discovery-method-kubernetes-api)
+to find peer nodes.
diff --git a/samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml b/samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml
new file mode 100644
index 000000000..6e3995e31
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml
@@ -0,0 +1,82 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app: appka
+ name: appka
+ namespace: appka-1
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: appka
+ template:
+ metadata:
+ labels:
+ app: appka
+ actorSystemName: appka
+ spec:
+ containers:
+ - name: appka
+ image: akka-sample-cluster-kubernetes:latest
+ # remove for real clusters, useful for minikube
+ imagePullPolicy: Never
+ readinessProbe:
+ httpGet:
+ path: /ready
+ port: management
+ periodSeconds: 10
+ failureThreshold: 3
+ initialDelaySeconds: 10
+ livenessProbe:
+ httpGet:
+ path: "/alive"
+ port: management
+ periodSeconds: 10
+ failureThreshold: 5
+ initialDelaySeconds: 20
+ ports:
+ # akka remoting
+ - name: remoting
+ containerPort: 2552
+ protocol: TCP
+ # akka-management and bootstrap
+ - name: management
+ containerPort: 8558
+ protocol: TCP
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ env:
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: REQUIRED_CONTACT_POINT_NR
+ value: "2"
+
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: pod-reader
+ namespace: appka-1
+rules:
+ - apiGroups: [""] # "" indicates the core API group
+ resources: ["pods"]
+ verbs: ["get", "watch", "list"]
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: read-pods
+ namespace: appka-1
+subjects:
+ # Note the `name` line below. The first default refers to the namespace. The second refers to the service account name.
+ # For instance, `name: system:serviceaccount:myns:default` would refer to the default service account in namespace `myns`
+ - kind: User
+ name: system:serviceaccount:appka-1:default
+roleRef:
+ kind: Role
+ name: pod-reader
+ apiGroup: rbac.authorization.k8s.io
diff --git a/samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json b/samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json
new file mode 100644
index 000000000..466d18c92
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json
@@ -0,0 +1,11 @@
+{
+ "kind": "Namespace",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "appka-1",
+ "labels": {
+ "name": "appka-1"
+ }
+ }
+}
+
diff --git a/samples/akka-sample-cluster-kubernetes-java/pom.xml b/samples/akka-sample-cluster-kubernetes-java/pom.xml
new file mode 100644
index 000000000..8f1ff2be5
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/pom.xml
@@ -0,0 +1,152 @@
+
+ 4.0.0
+ com.lightbend
+ akka-sample-cluster-kubernetes
+ 1.0
+ akka-sample-cluster-kubernetes-java
+ Akka Sample for forming a Cluster in Kubernetes
+
+
+ 17
+ 17
+ UTF-8
+ 2.9.2
+ 1.5.1
+ 10.6.2
+ 2.13
+ ${git.commit.time}-${git.commit.id.abbrev}
+
+
+
+
+ akka-repository
+ Akka library repository
+ https://repo.akka.io/maven
+
+
+
+
+ akka-repository
+ Akka library repository
+ https://repo.akka.io/maven
+
+
+
+
+
+ com.typesafe.akka
+ akka-http_${scala.binary.version}
+ ${akka-http.version}
+
+
+ com.typesafe.akka
+ akka-http-spray-json_${scala.binary.version}
+ ${akka-http.version}
+
+
+ com.typesafe.akka
+ akka-cluster-typed_${scala.binary.version}
+ ${akka.version}
+
+
+ com.typesafe.akka
+ akka-cluster-sharding-typed_${scala.binary.version}
+ ${akka.version}
+
+
+ com.typesafe.akka
+ akka-slf4j_${scala.binary.version}
+ ${akka.version}
+
+
+ com.typesafe.akka
+ akka-stream-typed_${scala.binary.version}
+ ${akka.version}
+
+
+ com.typesafe.akka
+ akka-discovery_${scala.binary.version}
+ ${akka.version}
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.13
+
+
+ com.lightbend.akka.management
+ akka-management-cluster-bootstrap_${scala.binary.version}
+ ${akka-management.version}
+
+
+ com.lightbend.akka.discovery
+ akka-discovery-kubernetes-api_${scala.binary.version}
+ ${akka-management.version}
+
+
+ com.lightbend.akka.management
+ akka-management-cluster-http_${scala.binary.version}
+ ${akka-management.version}
+
+
+
+
+
+ io.fabric8
+ docker-maven-plugin
+ 0.42.0
+
+
+
+ %a
+
+ docker.io/library/eclipse-temurin:17.0.8.1_1-jre
+
+ ${version.number}
+
+
+
+ java
+ -cp
+ /maven/*
+ akka.cluster.bootstrap.demo.DemoApp
+
+
+
+ artifact-with-dependencies
+
+
+
+
+
+
+
+ build-docker-image
+ package
+
+ build
+
+
+
+
+
+ pl.project13.maven
+ git-commit-id-plugin
+ 4.0.0
+
+
+ validate
+
+ revision
+
+
+
+
+ yyyyMMdd-HHmmss
+ ${project.basedir}/.git
+ false
+
+
+
+
+
diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java
new file mode 100644
index 000000000..272c480bc
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 Lightbend Inc.
+ */
+package akka.cluster.bootstrap.demo;
+
+import akka.actor.typed.ActorRef;
+import akka.actor.typed.ActorSystem;
+import akka.actor.typed.Behavior;
+import akka.actor.typed.javadsl.Adapter;
+import akka.actor.typed.javadsl.Behaviors;
+import akka.cluster.ClusterEvent;
+import akka.cluster.typed.Cluster;
+import akka.cluster.typed.Subscribe;
+import akka.http.javadsl.ConnectHttp;
+import akka.http.javadsl.Http;
+import static akka.http.javadsl.server.Directives.*;
+import akka.management.cluster.bootstrap.ClusterBootstrap;
+import akka.management.scaladsl.AkkaManagement;
+import akka.stream.Materializer;
+
+public class DemoApp {
+
+ static class MemberEventLogger {
+ public static Behavior create() {
+ return Behaviors.setup(context -> {
+ Cluster cluster = Cluster.get(context.getSystem());
+
+ context.getLog().info("Started [{}], cluster.selfAddress = {})",
+ context.getSystem(),
+ cluster.selfMember().address());
+
+ cluster.subscriptions().tell(new Subscribe<>(context.getSelf(), ClusterEvent.MemberEvent.class));
+
+ return Behaviors.receiveMessage(event -> {
+ context.getLog().info("MemberEvent: {}", event);
+ return Behaviors.same();
+ });
+ });
+ }
+ }
+
+ static class Guardian {
+ public static Behavior create() {
+ return Behaviors.setup(context -> {
+ final akka.actor.ActorSystem classicSystem = Adapter.toClassic(context.getSystem());
+ Materializer mat = Materializer.matFromSystem(classicSystem);
+
+ Http.get(classicSystem).bindAndHandle(complete("Hello world")
+ .flow(classicSystem, mat), ConnectHttp.toHost("0.0.0.0", 8080), mat)
+ .whenComplete((binding, failure) -> {
+ if (failure == null) {
+ classicSystem.log().info("HTTP server now listening at port 8080");
+ } else {
+ classicSystem.log().error(failure, "Failed to bind HTTP server, terminating.");
+ classicSystem.terminate();
+ }
+ });
+
+ context.spawn(MemberEventLogger.create(), "listener");
+
+ AkkaManagement.get(classicSystem).start();
+ ClusterBootstrap.get(classicSystem).start();
+
+ return Behaviors.empty();
+ });
+ }
+ }
+
+ public static void main(String[] args) {
+ ActorSystem.create(Guardian.create(), "Appka");
+ }
+
+}
\ No newline at end of file
diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java
new file mode 100644
index 000000000..b42430d92
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java
@@ -0,0 +1,22 @@
+package akka.cluster.bootstrap.demo;
+
+import akka.actor.ActorSystem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Supplier;
+
+public class DemoHealthCheck implements Supplier> {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ public DemoHealthCheck(ActorSystem system) {
+ }
+
+ @Override
+ public CompletionStage get() {
+ log.info("DemoHealthCheck called");
+ return CompletableFuture.completedFuture(true);
+ }
+}
diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf
new file mode 100644
index 000000000..8ab258e39
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf
@@ -0,0 +1,36 @@
+akka {
+ loglevel = "DEBUG"
+ actor.provider = cluster
+
+ coordinated-shutdown.exit-jvm = on
+
+ cluster {
+ shutdown-after-unsuccessful-join-seed-nodes = 60s
+ downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider"
+ }
+}
+
+#management-config
+akka.management {
+ cluster.bootstrap {
+ contact-point-discovery {
+ # For the kubernetes API this value is substributed into the %s in pod-label-selector
+ service-name = "appka"
+
+ # pick the discovery method you'd like to use:
+ discovery-method = kubernetes-api
+
+ required-contact-point-nr = 2
+ required-contact-point-nr = ${?REQUIRED_CONTACT_POINT_NR}
+ }
+ }
+}
+#management-config
+
+akka.management {
+ health-checks {
+ readiness-checks {
+ example-ready = "akka.cluster.bootstrap.demo.DemoHealthCheck"
+ }
+ }
+}
diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml
new file mode 100644
index 000000000..058a3ff5e
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ System.out
+
+ [%date{ISO8601}] [%level] [%logger] [%marker] [%thread] - %msg MDC: {%mdc}%n
+
+
+
+
+ 8192
+ true
+
+
+
+
+
+
+
+
diff --git a/samples/akka-sample-cluster-kubernetes-scala/README.md b/samples/akka-sample-cluster-kubernetes-scala/README.md
new file mode 100644
index 000000000..4df9ea723
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/README.md
@@ -0,0 +1,60 @@
+# akka-sample-cluster-kubernetes-scala
+akka sample cluster with kubernetes discovery in scala
+
+This is an example SBT project showing how to create an Akka Cluster on
+Kubernetes.
+
+## Kubernetes Instructions
+
+## Starting
+
+First, package the application and make it available locally as a docker image:
+
+ sbt Docker/publishLocal
+
+Then `akka-cluster.yml` should be sufficient to deploy a 2-node Akka Cluster, after
+creating a namespace for it:
+
+ kubectl apply -f kubernetes/namespace.json
+ kubectl config set-context --current --namespace=appka-1
+ kubectl apply -f kubernetes/akka-cluster.yml
+
+To check what you have done in Kubernetes so far, you can do:
+
+ kubectl get deployments
+ kubectl get pods
+ kubectl get replicasets
+ kubectl cluster-info dump
+ kubectl logs appka-79c98cf745-abcdee # pod name
+
+Finally, create a service so that you can then test [http://127.0.0.1:8080](http://127.0.0.1:8080)
+for 'hello world':
+
+ kubectl expose deployment appka --type=LoadBalancer --name=appka-service
+ kubectl port-forward svc/appka-service 8080:8080
+
+To wipe everything clean and start over, do:
+
+ kubectl delete namespaces appka-1
+
+## Running in a real Kubernetes cluster
+
+#### Publish to a registry the cluster can access e.g. Dockerhub with the kubakka user
+
+The app image must be in a registry the cluster can see. The build.sbt uses DockerHub by default.
+Start with `sbt -Ddocker.registry=your-registry` if your cluster can't access DockerHub.
+
+The user for the registry is defined with `sbt -Ddocker.username=your-user`
+
+To push an image to docker hub run:
+
+ `sbt -Ddocker.username=your-user docker:publish`
+
+And remove the `imagePullPolicy: Never` from the deployments. Then you can use the same `kubectl` commands
+as described in the [Starting](#starting) section.
+
+## How it works
+
+This example uses [Akka Cluster Bootstrap](https://doc.akka.io/docs/akka-management/current/bootstrap/index.html)
+to initialize the cluster, using the [Kubernetes API discovery mechanism](https://doc.akka.io/docs/akka-management/current/discovery/index.html#discovery-method-kubernetes-api)
+to find peer nodes.
diff --git a/samples/akka-sample-cluster-kubernetes-scala/build.sbt b/samples/akka-sample-cluster-kubernetes-scala/build.sbt
new file mode 100644
index 000000000..a7e9c24ac
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/build.sbt
@@ -0,0 +1,46 @@
+ThisBuild / organization := "com.lightbend"
+
+name := "akka-sample-cluster-kubernetes"
+
+scalaVersion := "2.13.13"
+lazy val akkaHttpVersion = "10.6.2"
+lazy val akkaVersion = "2.9.2"
+lazy val akkaManagementVersion = "1.5.1"
+
+// make version compatible with docker for publishing
+ThisBuild / dynverSeparator := "-"
+
+scalacOptions := Seq("-feature", "-unchecked", "-deprecation", "-encoding", "utf8")
+classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars
+run / fork := true
+Compile / run / fork := true
+Compile / run / mainClass := Some("akka.sample.cluster.kubernetes.DemoApp")
+
+enablePlugins(JavaServerAppPackaging, DockerPlugin)
+
+dockerExposedPorts := Seq(8080, 8558, 25520)
+dockerUpdateLatest := true
+dockerUsername := sys.props.get("docker.username")
+dockerRepository := sys.props.get("docker.registry")
+dockerBaseImage := "adoptopenjdk:11-jre-hotspot"
+
+resolvers += "Akka library repository".at("https://repo.akka.io/maven")
+
+libraryDependencies ++= {
+ Seq(
+ "com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
+ "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
+ "com.typesafe.akka" %% "akka-cluster-typed" % akkaVersion,
+ "com.typesafe.akka" %% "akka-cluster-sharding-typed" % akkaVersion,
+ "com.typesafe.akka" %% "akka-stream-typed" % akkaVersion,
+ "com.typesafe.akka" %% "akka-discovery" % akkaVersion,
+ "ch.qos.logback" % "logback-classic" % "1.2.13",
+ "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % akkaManagementVersion,
+ "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % akkaManagementVersion,
+ "com.lightbend.akka.management" %% "akka-management-cluster-http" % akkaManagementVersion,
+ "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test",
+ "com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion % Test,
+ "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test,
+ "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test,
+ "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test)
+}
diff --git a/samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml
new file mode 100644
index 000000000..703777919
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml
@@ -0,0 +1,78 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app: appka
+ name: appka
+ namespace: appka-1
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: appka
+ template:
+ metadata:
+ labels:
+ app: appka
+ actorSystemName: appka
+ spec:
+ containers:
+ - name: appka
+ image: akka-sample-cluster-kubernetes:latest
+ # remove for real clusters, useful for minikube
+ imagePullPolicy: Never
+ readinessProbe:
+ httpGet:
+ path: /ready
+ port: management
+ periodSeconds: 10
+ failureThreshold: 3
+ initialDelaySeconds: 10
+ livenessProbe:
+ httpGet:
+ path: "/alive"
+ port: management
+ periodSeconds: 10
+ failureThreshold: 5
+ initialDelaySeconds: 20
+ ports:
+ # akka-management and bootstrap
+ - name: management
+ containerPort: 8558
+ protocol: TCP
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ env:
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: REQUIRED_CONTACT_POINT_NR
+ value: "3"
+
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: pod-reader
+ namespace: appka-1
+rules:
+- apiGroups: [""] # "" indicates the core API group
+ resources: ["pods"]
+ verbs: ["get", "watch", "list"]
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: read-pods
+ namespace: appka-1
+subjects:
+# Note the `name` line below. The first default refers to the namespace. The second refers to the service account name.
+# For instance, `name: system:serviceaccount:myns:default` would refer to the default service account in namespace `myns`
+- kind: User
+ name: system:serviceaccount:appka-1:default
+roleRef:
+ kind: Role
+ name: pod-reader
+ apiGroup: rbac.authorization.k8s.io
diff --git a/samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json
new file mode 100644
index 000000000..466d18c92
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json
@@ -0,0 +1,11 @@
+{
+ "kind": "Namespace",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "appka-1",
+ "labels": {
+ "name": "appka-1"
+ }
+ }
+}
+
diff --git a/samples/akka-sample-cluster-kubernetes-scala/project/build.properties b/samples/akka-sample-cluster-kubernetes-scala/project/build.properties
new file mode 100644
index 000000000..abbbce5da
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.9.8
diff --git a/samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt b/samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt
new file mode 100644
index 000000000..8bab11d60
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt
@@ -0,0 +1,4 @@
+resolvers += "Akka library repository".at("https://repo.akka.io/maven")
+
+addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16")
+addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1")
diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf
new file mode 100644
index 000000000..3c8624381
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf
@@ -0,0 +1,31 @@
+akka {
+ loglevel = "DEBUG"
+ actor.provider = cluster
+
+ coordinated-shutdown.exit-jvm = on
+
+ cluster {
+ shutdown-after-unsuccessful-join-seed-nodes = 60s
+ }
+}
+
+#management-config
+akka.management {
+ cluster.bootstrap {
+ contact-point-discovery {
+ # pick the discovery method you'd like to use:
+ discovery-method = kubernetes-api
+
+ required-contact-point-nr = ${REQUIRED_CONTACT_POINT_NR}
+ }
+ }
+}
+#management-config
+
+akka.management {
+ health-checks {
+ readiness-checks {
+ example-ready = "akka.sample.cluster.kubernetes.DemoHealthCheck"
+ }
+ }
+}
diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml
new file mode 100644
index 000000000..058a3ff5e
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ System.out
+
+ [%date{ISO8601}] [%level] [%logger] [%marker] [%thread] - %msg MDC: {%mdc}%n
+
+
+
+
+ 8192
+ true
+
+
+
+
+
+
+
+
diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala
new file mode 100644
index 000000000..8ce065843
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala
@@ -0,0 +1,37 @@
+package akka.sample.cluster.kubernetes
+
+import akka.actor.typed.ActorSystem
+import akka.actor.typed.scaladsl.Behaviors
+import akka.cluster.ClusterEvent
+import akka.cluster.typed.{ Cluster, Subscribe }
+import akka.http.scaladsl.Http
+import akka.http.scaladsl.server.Directives._
+import akka.management.cluster.bootstrap.ClusterBootstrap
+import akka.management.javadsl.AkkaManagement
+import akka.{ actor => classic }
+
+object DemoApp extends App {
+
+ ActorSystem[Nothing](Behaviors.setup[Nothing] { context =>
+ import akka.actor.typed.scaladsl.adapter._
+ implicit val classicSystem: classic.ActorSystem = context.system.toClassic
+ implicit val ec = context.system.executionContext
+
+ val cluster = Cluster(context.system)
+ context.log.info("Started [" + context.system + "], cluster.selfAddress = " + cluster.selfMember.address + ")")
+
+ Http().newServerAt("0.0.0.0", 8080).bind(complete("Hello world"))
+
+ // Create an actor that handles cluster domain events
+ val listener = context.spawn(Behaviors.receive[ClusterEvent.MemberEvent]((ctx, event) => {
+ ctx.log.info("MemberEvent: {}", event)
+ Behaviors.same
+ }), "listener")
+
+ Cluster(context.system).subscriptions ! Subscribe(listener, classOf[ClusterEvent.MemberEvent])
+
+ AkkaManagement.get(classicSystem).start()
+ ClusterBootstrap.get(classicSystem).start()
+ Behaviors.empty
+ }, "appka")
+}
diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala
new file mode 100644
index 000000000..f29de3e67
--- /dev/null
+++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala
@@ -0,0 +1,16 @@
+package akka.sample.cluster.kubernetes
+
+import scala.concurrent.Future
+
+import akka.actor.ActorSystem
+import org.slf4j.LoggerFactory
+
+// Enabled in application.conf
+class DemoHealthCheck(system: ActorSystem) extends (() => Future[Boolean]) {
+ private val log = LoggerFactory.getLogger(getClass)
+
+ override def apply(): Future[Boolean] = {
+ log.info("DemoHealthCheck called")
+ Future.successful(true)
+ }
+}
diff --git a/scripts/prepare-downloads.sh b/scripts/prepare-downloads.sh
new file mode 100755
index 000000000..6b59a0707
--- /dev/null
+++ b/scripts/prepare-downloads.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+
+set -e
+
+declare -r samples_sources="${PWD}/samples"
+declare -r docs_attachments="${PWD}/docs/src/main/paradox/attachments"
+declare -r target_temporal_attachments="${PWD}/target/docs/_attachments"
+
+declare -r temporal_folder="${PWD}/target/zips"
+
+function sed_command() {
+ local platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
+
+ if [ "${platform}" != "darwin" ]; then
+ echo "sed"
+ else
+ # using gnu-sed on Mac
+ echo "gsed"
+ fi
+}
+
+## Remove the tags used by Paradox snippets from the codebase in the current folder
+function removeTags() {
+ ## remove tags from code
+ find . -type f -print0 | xargs -0 $(sed_command) -i "s/\/\/ #.*//g"
+}
+
+
+## Cleanup the temporal folder from previous executions
+function prepareTemporalFolder() {
+ rm -rf ${temporal_folder}
+ mkdir -p ${temporal_folder}
+}
+
+## Copy a folder with some code into the temporal folder. The
+## copied folder will be renamed to the folder name we want the
+## user to see when unzipping the file.
+## source_name -> folder in `examples`
+## target_name -> folder name the user should see (must not use a numeric prefix of a laguage suffix)
+function fetchProject() {
+ source_name=$1
+ target_name=$2
+ echo "Fetching content from [$1] to [$2]"
+ cp -a ${source_name} ${temporal_folder}/${target_name}
+ rm -rf ${temporal_folder}/${target_name}/target
+}
+
+## Zip the contents in $temporal_folder and create the
+## attachment file (aka, the zip file on the appropriate location)
+function zipAndAttach() {
+ zip_name=$1
+ temporal_attachments=$2
+ echo "Preparing zip $1"
+ pushd ${temporal_folder}
+ removeTags
+ zip --quiet -r ${zip_name} *
+ cp ${zip_name} ${temporal_attachments}
+ echo "Prepared attachment at ${zip_name}"
+ popd
+}
+
+mkdir -p ${docs_attachments}
+mkdir -p ${target_temporal_attachments}
+
+## akka-sample-cluster-kubernetes-java zip file
+prepareTemporalFolder
+fetchProject ${samples_sources}/akka-sample-cluster-kubernetes-java akka-sample-cluster-kubernetes
+zipAndAttach ${docs_attachments}/akka-sample-cluster-kubernetes-java.zip ${target_temporal_attachments}
+
+## akka-sample-cluster-kubernetes-scala zip file
+prepareTemporalFolder
+fetchProject ${samples_sources}/akka-sample-cluster-kubernetes-scala akka-sample-cluster-kubernetes
+zipAndAttach ${docs_attachments}/akka-sample-cluster-kubernetes-scala.zip ${target_temporal_attachments}