Skip to content

Commit

Permalink
Version 0.2.4-beta3 Release
Browse files Browse the repository at this point in the history
Version 0.2.4-beta3 Release
  • Loading branch information
Arkatufus authored May 5, 2022
2 parents 802545f + 49142c7 commit 5092dff
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 28 deletions.
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#### 0.2.4-beta3 May 5 2022 ####

* [Fix async routing in netcoreapp3.1](https://github.com/akkadotnet/Akka.Management/pull/563)

#### 0.2.4-beta2 April 14 2022 ####

* Update to [Akka.NET v1.4.37](https://github.com/akkadotnet/akka.net/releases/tag/1.4.37)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task EmptyListIfNotPartOfCluster()
context.Request.Method = HttpMethods.Get;
context.Request.Path = ClusterBootstrapRequests.BootstrapSeedNodes("").ToString();

var requestContext = new RequestContext(HttpRequest.Create(context.Request), Sys);
var requestContext = new RequestContext(await HttpRequest.CreateAsync(context.Request), Sys);
var response = (RouteResult.Complete) await _httpBootstrap.Routes.Concat()(requestContext);
response.Response.Entity.DataBytes.ToString().Should().Contain("\"Nodes\":[]");
}
Expand All @@ -75,7 +75,7 @@ public async Task IncludeSeedsWhenPartOfCluster()
context.Request.Method = HttpMethods.Get;
context.Request.Path = ClusterBootstrapRequests.BootstrapSeedNodes("");

var requestContext = new RequestContext(HttpRequest.Create(context.Request), Sys);
var requestContext = new RequestContext(await HttpRequest.CreateAsync(context.Request), Sys);
var response = (RouteResult.Complete) await _httpBootstrap.Routes.Concat()(requestContext);

var responseString = response.Response.Entity.DataBytes.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageReference Include="Akka.TestKit.Xunit2" Version="$(AkkaVersion)" />
<PackageReference Include="FluentAssertions" Version="$(FluentAssertionVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="WireMock.Net" Version="1.4.40" />
<PackageReference Include="WireMock.Net" Version="1.4.41" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
185 changes: 185 additions & 0 deletions src/coordination/kubernetes/Akka.Coordination.KubernetesApi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Kubernetes Lease
This module is an implementation of an Akka Coordination Lease backed by a [Custom Resource Definition (CRD)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) in Kubernetes. Resources in Kubernetes offer concurrency control and consistency that have been used to build a distributed lease/lock.

A lease can be used for:

* [Split Brain Resolver](https://getakka.net/articles/clustering/split-brain-resolver.html). An additional safety measure so that only one SBR instance can make the decision to remain up.
* [Cluster Singleton](https://getakka.net/articles/clustering/cluster-singleton.html). A singleton manager can be configured to acquire a lease before creating the singleton.
* [Cluster Sharding](https://getakka.net/articles/clustering/cluster-sharding.html). Each Shard can be configured to acquire a lease before creating entity actors.

In all cases the use of the lease increases the consistency of the feature. However, as the Kubernetes API server and its backing `etcd` cluster can also be subject to failure and network issues any use of this lease can reduce availability.

## Lease Instances

* With Split Brain Resolver there will be one lease per Akka Cluster
* With multiple Akka Clusters using SBRs in the same namespace, you must ensure different `ActorSystem` names because they all need a separate lease.
* With Cluster Sharding and Cluster Singleton there will be more leases
* For Cluster Singleton there will be one per singleton.
* For Cluster Sharding, there will be one per shard per type.

## Configuring

### Creating the Custom Resource Definition for the lease

This requires admin privileges to your Kubernetes / Open Shift cluster but only needs doing once.

Kubernetes:
```
kubectl apply -f lease.yml
```

Where lease.yml contains:
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: leases.akka.io
spec:
group: akka.io
versions:
- name: v1
storage: true
served: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
owner:
type: string
version:
type: string
time:
type: integer
scope: Namespaced
names:
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: Lease
listKind: LeaseList
# singular name to be used as an alias on the CLI and for display
singular: lease
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: leases
```
### Role based access control
Each pod needs permission to read/create and update lease resources. They only need access for the namespace they are in.
An example RBAC that can be used:
```yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: lease-access
rules:
- apiGroups: ["akka.io"]
resources: ["leases"]
verbs: ["get", "create", "update", "list"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: lease-access
subjects:
- kind: User
name: system:serviceaccount:<YOUR NAMSPACE>:default
roleRef:
kind: Role
name: lease-access
apiGroup: rbac.authorization.k8s.io
```
This defines a `Role` that is allows to `get`, `create` and `update` lease objects and a `RoleBinding` that gives the default service user this role in `<YOUR NAMESPACE>`.

Future versions may also require delete access for cleaning up old resources. Current uses within Akka only create a single lease so cleanup is not an issue.

To avoid giving an application the access to create new leases an empty lease can be created in the same namespace as the application with:

```shell
kubelctl create -f sbr-lease.yml -n <YOUR_NAMESPACE>
```

Where sbr-lease.yml contains:

```yaml
apiVersion: "akka.io/v1"
kind: Lease
metadata:
name: <YOUR_ACTORSYSTEM_NAME>-akka-sbr
spec:
owner: ""
time: 0
```

> __NOTE__
>
> The lease gets created only during an actual Split Brain.

### Enable in SBR

To enable the lease for use within SBR:

```
akka.cluster {
downing-provider-class = "Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster"
split-brain-resolver {
active-strategy = lease-majority
lease-majority {
lease-implementation = "akka.coordination.lease.kubernetes"
}
}
}
```

## Full configuration options

```
akka.coordination.lease.kubernetes {

lease-class = "Akka.Coordination.KubernetesApi.KubernetesLease, Akka.Coordination.KubernetesApi"

api-ca-path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
api-token-path = "/var/run/secrets/kubernetes.io/serviceaccount/token"

api-service-host-env-name = "KUBERNETES_SERVICE_HOST"
api-service-port-env-name = "KUBERNETES_SERVICE_PORT"

# Namespace file path. The namespace is to create the lock in. Can be overridden by "namespace"
#
# If this path doesn't exist, the namespace will default to "default".
namespace-path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"

# Namespace to create the lock in. If set to something other than "<namespace>" then overrides any value
# in "namespace-path"
namespace = "<namespace>"

# How often to write time into CRD so that if the holder crashes
# another node can take the lease after a given timeout. If left blank then the default is
# max(5s, heartbeat-timeout / 10) which will be 12s with the default heartbeat-timeout
heartbeat-interval = ""

# How long a lease must not be updated before another node can assume
# the holder has crashed.
# If the lease holder hasn't crashed its next heart beat will fail due to the version
# having been updated
heartbeat-timeout = 120s

# The individual timeout for each HTTP request. Defaults to 2/5 of the lease-operation-timeout
# Can't be greater than then lease-operation-timeout
api-service-request-timeout = ""

# Use TLS & auth token for communication with the API server
# set to false for plain text with no auth
secure-api-server = true

# The amount of time to wait for a lease to be aquired or released. This includes all requests to the API
# server that are required. If this timeout is hit then the lease *may* be taken due to the response being lost
# on the way back from the API server but will be reported as not taken and can be safely retried.
lease-operation-timeout = 5s
}
```
36 changes: 36 additions & 0 deletions src/discovery/kubernetes/Akka.Discovery.KubernetesApi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ cluster.RegisterOnMemberUp(() => {
});
```

## Role-Based Access Control

If your Kubernetes cluster has [Role-Based Access Control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) enabled, you’ll also have to grant the Service Account that your pods run under access to list pods. The following configuration can be used as a starting point. It creates a Role, pod-reader, which grants access to query pod information. It then binds the default Service Account to the Role by creating a RoleBinding. Adjust as necessary.

```yaml
#
# Create a role, `pod-reader`, that can list pods and
# bind the default service account in the namespace
# that the binding is deployed to to that role.
#

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: pod-reader
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
subjects:
# Uses the default service account.
# Consider creating a dedicated service account to run your
# Akka Cluster services and binding the role to that one.
- kind: ServiceAccount
name: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
## Configuration
### Kubernetes YAML Configuration
Below is an example of a YAML example taken from our [integration sample](https://github.com/akkadotnet/akka.net-integration-tests/tree/master/src/ClusterBootstrap).
Expand Down
2 changes: 1 addition & 1 deletion src/management/Akka.Http.Shim/AkkaRoutingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public async Task Invoke(HttpContext context)
throw new ArgumentNullException(nameof(context));
}

var requestContext = new RequestContext(Dsl.Model.HttpRequest.Create(context.Request), _system);
var requestContext = new RequestContext(await Dsl.Model.HttpRequest.CreateAsync(context.Request), _system);

var response = await _routes(requestContext);
switch (response)
Expand Down
34 changes: 18 additions & 16 deletions src/management/Akka.Http.Shim/Dsl/Model/HttpMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Collections.Immutable;
using System.Net;
using System.Threading.Tasks;
using Akka.Annotations;
using Akka.IO;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -66,22 +67,22 @@ public sealed class HttpRequest : HttpMessage<HttpRequest>

private readonly Microsoft.AspNetCore.Http.HttpRequest _request;

public static HttpRequest Create(Microsoft.AspNetCore.Http.HttpRequest request) =>
new HttpRequest(request);
public static async Task<HttpRequest> CreateAsync(Microsoft.AspNetCore.Http.HttpRequest request)
{
var input = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(input, 0, input.Length).ConfigureAwait(false);
var bytes = ByteString.FromBytes(input);
return new HttpRequest(request, bytes);
}

private HttpRequest(Microsoft.AspNetCore.Http.HttpRequest request)

private HttpRequest(Microsoft.AspNetCore.Http.HttpRequest request, ByteString input)
{
_request = request;
var input = new byte[Convert.ToInt32(request.ContentLength)];
#if NET5_0
request.Body.ReadAsync(input, 0, input.Length).Wait();
#else
request.Body.Read(input, 0, input.Length);
#endif

Entity = new RequestEntity(request.ContentType, ByteString.FromBytes(input));
Entity = new RequestEntity(request.ContentType, input);
}

/*
/// <inheritdoc />
public override HttpRequest WithEntity(RequestEntity entity) => Copy(entity: entity);
Expand Down Expand Up @@ -126,7 +127,8 @@ public sealed class HttpResponse : HttpMessage<HttpResponse>
/// <summary>
/// Returns a default response to be changed using the `WithX` methods.
/// </summary>
public static HttpResponse Create(int status = 200, ImmutableList<HttpHeader> headers = null, ResponseEntity entity = null, string protocol = "HTTP/1.1") =>
public static HttpResponse Create(int status = 200, ImmutableList<HttpHeader> headers = null,
ResponseEntity entity = null, string protocol = "HTTP/1.1") =>
new HttpResponse(status, headers, entity ?? ResponseEntity.Empty, protocol);

private HttpResponse(int status, ImmutableList<HttpHeader> headers, ResponseEntity entity, string protocol)
Expand Down Expand Up @@ -155,11 +157,11 @@ private HttpResponse Copy(
*/

private bool Equals(HttpResponse other) => Status == other.Status &&
Equals(Headers, other.Headers) &&
Equals(Entity, other.Entity) &&
Protocol == other.Protocol;
Equals(Headers, other.Headers) &&
Equals(Entity, other.Entity) &&
Protocol == other.Protocol;

public override bool Equals(object obj) =>
public override bool Equals(object obj) =>
ReferenceEquals(this, obj) || obj is HttpResponse other && Equals(other);

public override int GetHashCode()
Expand Down
Loading

0 comments on commit 5092dff

Please sign in to comment.