diff --git a/.checkov.yml b/.checkov.yml new file mode 100644 index 0000000..04ca71b --- /dev/null +++ b/.checkov.yml @@ -0,0 +1,3 @@ +skip-check: + - CKV_ARGO_2 + - CKV_DOCKER_2 diff --git a/.dockerignore b/.dockerignore index 6c057ab..598f543 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,5 +2,7 @@ !src !.editorconfig !*.sln +!Directory.Build.props +!tests/chaos src/**/bin src/**/obj diff --git a/.github/workflows/nightly-chaos.yaml b/.github/workflows/nightly-chaos.yaml new file mode 100644 index 0000000..72e724e --- /dev/null +++ b/.github/workflows/nightly-chaos.yaml @@ -0,0 +1,80 @@ +name: nightly chaos testing + +on: + schedule: + # daily at 04:07. + - cron: "07 04 * * *" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + chaos-testing: + name: chaos testing + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3 + with: + fetch-depth: 0 + + - name: Build stress testing image + id: build-image + uses: docker/build-push-action@c84f38281176d4c9cdb1626ffafcd6b3911b5d94 # tag=v3 + with: + push: false + load: true + tags: ghcr.io/miracum/vfps/stress-test:v1 + cache-from: type=gha + cache-to: type=gha,mode=max + target: stress-test + + - name: Create KinD cluster + uses: helm/kind-action@9e8295d178de23cbfbd8fa16cf844eec1d773a07 # tag=v1.4.0 + with: + cluster_name: kind + + - name: Load stress-test image into KinD + run: | + kind load docker-image ghcr.io/miracum/vfps/stress-test:v1 + + - name: Install prerequisites + working-directory: tests/chaos + run: | + curl -sL -o - https://github.com/argoproj/argo-workflows/releases/download/v3.4.3/argo-linux-amd64.gz | gunzip > argo + + kubectl create ns vfps + + helm repo add chaos-mesh https://charts.chaos-mesh.org + helm upgrade --install chaos-mesh chaos-mesh/chaos-mesh \ + --create-namespace \ + --wait \ + -n chaos-mesh \ + --set chaosDaemon.runtime=containerd \ + --set chaosDaemon.socketPath='/run/containerd/containerd.sock' \ + --version 2.4.3 + + kubectl apply -f chaos-mesh-rbac.yaml + + helm repo add argo https://argoproj.github.io/argo-helm + helm upgrade --install argo-workflows argo/argo-workflows \ + --create-namespace \ + --wait \ + -n argo-workflows \ + -f argo-workflows-values.yaml + + - name: Install vfps + working-directory: tests/chaos + run: | + helm repo add miracum https://miracum.github.io/charts + helm upgrade --install \ + -n vfps \ + -f vfps-values.yaml \ + --wait \ + --version=^1.0.0 \ + vfps miracum/vfps + + - name: Run chaos testing workflow + working-directory: tests/chaos + run: | + ./argo submit tests/chaos/argo-workflow.yaml -n vfps --wait diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index ce5a52a..f227020 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -19,7 +19,8 @@ jobs: with: token: ${{ secrets.RELEASE_PLEASE_GITHUB_TOKEN }} release-type: simple - package-name: release-please-action + extra-files: | + src/Directory.Build.props changelog-types: | [ { "type": "feat", "section": "Features" }, diff --git a/.gitignore b/.gitignore index 47875c5..ae48773 100644 --- a/.gitignore +++ b/.gitignore @@ -405,3 +405,6 @@ coverage/ coveragereport/ megalinter-reports/ + +# used by NBomber +reports/ diff --git a/Dockerfile b/Dockerfile index 12c4814..dbf533d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,10 @@ WORKDIR /build ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \ PATH="/root/.dotnet/tools:${PATH}" -RUN dotnet tool install --global dotnet-ef +RUN dotnet tool install --global dotnet-ef --version=7.0.0 -COPY src/Vfps/Vfps.csproj src/Vfps/Vfps.csproj +COPY src/Directory.Build.props src/ +COPY src/Vfps/Vfps.csproj src/Vfps/ RUN dotnet restore --runtime=linux-x64 src/Vfps/Vfps.csproj @@ -37,7 +38,6 @@ dotnet ef migrations bundle \ --project=src/Vfps/Vfps.csproj \ --startup-project=src/Vfps/Vfps.csproj \ --configuration=Release \ - --target-runtime=linux-x64 \ --verbose \ -o /build/efbundle EOF @@ -51,6 +51,31 @@ RUN dotnet test \ -l "console;verbosity=detailed" \ --settings=runsettings.xml +FROM build AS build-stress-test +WORKDIR /build/src/Vfps.LoadTests +RUN < + + net7.0 + 11.0 + enable + enable + latest + true + + 1.1.0 + miracum.org + A very fast and resource-efficient pseudonym service. + © miracum.org. All rights reserved. + en-US + miracum.org + + + + true + + diff --git a/src/Vfps.Benchmarks/Vfps.Benchmarks.csproj b/src/Vfps.Benchmarks/Vfps.Benchmarks.csproj index 4ff04c0..19a77eb 100644 --- a/src/Vfps.Benchmarks/Vfps.Benchmarks.csproj +++ b/src/Vfps.Benchmarks/Vfps.Benchmarks.csproj @@ -2,9 +2,6 @@ Exe - net7.0 - enable - enable false diff --git a/src/Vfps.IntegrationTests/Vfps.IntegrationTests.csproj b/src/Vfps.IntegrationTests/Vfps.IntegrationTests.csproj index 66f8be1..01f519b 100644 --- a/src/Vfps.IntegrationTests/Vfps.IntegrationTests.csproj +++ b/src/Vfps.IntegrationTests/Vfps.IntegrationTests.csproj @@ -1,9 +1,6 @@ - net7.0 - enable - enable false diff --git a/src/Vfps.LoadTests/Program.cs b/src/Vfps.LoadTests/Program.cs index 9a3b74c..1cdfe7f 100644 --- a/src/Vfps.LoadTests/Program.cs +++ b/src/Vfps.LoadTests/Program.cs @@ -1,54 +1,96 @@ +using System.Diagnostics; +using Grpc.Core; +using Grpc.Net.Client; +using Grpc.Net.Client.Configuration; +using NBomber.Contracts; using NBomber.CSharp; -using NBomber.Plugins.Http.CSharp; using NBomber.Plugins.Network.Ping; -using System.Net.Http.Json; +using Vfps.Protos; -var baseUrl = "https://localhost:7078/v1"; +var grpcAddress = new Uri(Environment.GetEnvironmentVariable("VFPS_GRPC_ADDRESS") ?? "http://localhost:8081"); -var httpFactory = HttpClientFactory.Create(); +var defaultMethodConfig = new MethodConfig +{ + Names = { MethodName.Default }, + RetryPolicy = new RetryPolicy + { + MaxAttempts = 3, + InitialBackoff = TimeSpan.FromSeconds(5), + MaxBackoff = TimeSpan.FromSeconds(30), + BackoffMultiplier = 2, + RetryableStatusCodes = { StatusCode.Unavailable, StatusCode.Internal } + } +}; -var createNamespace = Step.Create("create_namespace", - clientFactory: httpFactory, - execute: context => - { - var request = Http.CreateRequest("POST", baseUrl + "/namespaces") - .WithHeader("Accept", "application/json") - .WithBody(JsonContent.Create(new - { - Name = "load-test", - PseudonymLength = 32, - PseudonymGenerationMethod = 1, - })); - - return Http.Send(request, context); - }); +using var channel = GrpcChannel.ForAddress(grpcAddress, new GrpcChannelOptions() +{ + ServiceConfig = new ServiceConfig() + { + MethodConfigs = { defaultMethodConfig } + }, + Credentials = ChannelCredentials.Insecure, + UnsafeUseInsecureChannelCallCredentials = true, +}); + +var namespaceClient = new NamespaceService.NamespaceServiceClient(channel); +var pseudonymClient = new PseudonymService.PseudonymServiceClient(channel); + +var namespaceRequest = new NamespaceServiceCreateRequest() +{ + Name = "stress-test", + PseudonymGenerationMethod = PseudonymGenerationMethod.SecureRandomBase64UrlEncoded, + PseudonymLength = 16, + PseudonymPrefix = "stress-", +}; var createPseudonyms = Step.Create("create_pseudonyms", - clientFactory: httpFactory, - execute: context => + execute: async context => { - var request = Http.CreateRequest("POST", baseUrl + "/namespaces/load-test/pseudonyms") - .WithHeader("Accept", "application/json") - .WithBody(JsonContent.Create(new - { - OriginalValue = Guid.NewGuid().ToString(), - })); - - return Http.Send(request, context); + var request = new PseudonymServiceCreateRequest() + { + Namespace = namespaceRequest.Name, + OriginalValue = Guid.NewGuid().ToString(), + }; + + try + { + var response = await pseudonymClient.CreateAsync(request); + return Response.Ok(statusCode: 200, sizeBytes: request.CalculateSize() + response.CalculateSize()); + } + catch (RpcException exc) + { + context.Logger.Error(exc, "Pseudonym creation failed"); + return Response.Fail(); + } }); var scenario = ScenarioBuilder - .CreateScenario("create_namespace_and_pseudonyms", createNamespace, createPseudonyms) + .CreateScenario("stress_pseudonym_creation", createPseudonyms) + .WithInit(async context => + { + try + { + var response = await namespaceClient.CreateAsync(namespaceRequest); + } + catch (RpcException exc) when (exc.StatusCode == StatusCode.AlreadyExists) + { + context.Logger.Warning($"Namespace {namespaceRequest.Name} already exists. Continuing anyway."); + } + }) .WithWarmUpDuration(TimeSpan.FromSeconds(5)) .WithLoadSimulations( - Simulation.InjectPerSec(rate: 100, during: TimeSpan.FromSeconds(30)) + Simulation.RampConstant(copies: 10, during: TimeSpan.FromMinutes(4)), + Simulation.KeepConstant(copies: 100, during: TimeSpan.FromMinutes(4)), + Simulation.InjectPerSecRandom(minRate: 10, maxRate: 50, during: TimeSpan.FromMinutes(4)) ); // creates ping plugin that brings additional reporting data -var pingPluginConfig = PingPluginConfig.CreateDefault(new[] { "127.0.0.1" }); +var pingPluginConfig = PingPluginConfig.CreateDefault(new[] { grpcAddress.Host }); var pingPlugin = new PingPlugin(pingPluginConfig); -NBomberRunner +var stats = NBomberRunner .RegisterScenarios(scenario) .WithWorkerPlugins(pingPlugin) .Run(); + +Debug.Assert(stats.FailCount < 100); diff --git a/src/Vfps.LoadTests/Vfps.LoadTests.csproj b/src/Vfps.LoadTests/Vfps.LoadTests.csproj index ec1a52e..bd7e712 100644 --- a/src/Vfps.LoadTests/Vfps.LoadTests.csproj +++ b/src/Vfps.LoadTests/Vfps.LoadTests.csproj @@ -1,15 +1,13 @@ - Exe - net7.0 - enable - enable - - + + + + diff --git a/src/Vfps.Tests/Vfps.Tests.csproj b/src/Vfps.Tests/Vfps.Tests.csproj index 5edc155..7ee3245 100644 --- a/src/Vfps.Tests/Vfps.Tests.csproj +++ b/src/Vfps.Tests/Vfps.Tests.csproj @@ -1,9 +1,6 @@ - net7.0 - enable - enable false diff --git a/src/Vfps/Vfps.csproj b/src/Vfps/Vfps.csproj index 30fd5aa..92cbeef 100644 --- a/src/Vfps/Vfps.csproj +++ b/src/Vfps/Vfps.csproj @@ -1,15 +1,12 @@ - net7.0 - enable - enable true - - - + + + @@ -18,9 +15,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + @@ -32,6 +29,6 @@ - + diff --git a/tests/chaos/argo-workflow.yaml b/tests/chaos/argo-workflow.yaml new file mode 100644 index 0000000..c924394 --- /dev/null +++ b/tests/chaos/argo-workflow.yaml @@ -0,0 +1,74 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/argoproj/argo-workflows/v3.4.3/api/jsonschema/schema.json +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: vfps-chaos-workflow- +spec: + entrypoint: run-chaos-and-test + serviceAccountName: chaos-mesh-cluster-manager + templates: + - name: test + container: + image: ghcr.io/miracum/vfps/stress-test:v1 + imagePullPolicy: IfNotPresent + command: + - dotnet + args: + - /opt/vfps-stress/Vfps.LoadTests.dll + env: + - name: VFPS_GRPC_ADDRESS + value: dns:///vfps-headless:8081 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + runAsNonRoot: true + + - name: install-chaos + container: + image: ghcr.io/miracum/vfps/stress-test:v1 + imagePullPolicy: IfNotPresent + command: + - kubectl + args: + - apply + - -f + - /tmp/chaos.yaml + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + runAsNonRoot: true + + - name: delete-chaos + container: + image: ghcr.io/miracum/vfps/stress-test:v1 + imagePullPolicy: IfNotPresent + command: + - kubectl + args: + - delete + - -f + - /tmp/chaos.yaml + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + runAsNonRoot: true + + - name: run-chaos-and-test + dag: + tasks: + - name: test + template: test + - name: install-chaos + template: install-chaos + - name: delete-chaos + depends: "install-chaos && (test.Succeeded || test.Failed)" + template: delete-chaos diff --git a/tests/chaos/argo-workflows-values.yaml b/tests/chaos/argo-workflows-values.yaml new file mode 100644 index 0000000..d2b68d6 --- /dev/null +++ b/tests/chaos/argo-workflows-values.yaml @@ -0,0 +1,8 @@ +controller: + workflowNamespaces: + - default + - argo-workflows + - vfps + +server: + extraArgs: [--auth-mode=server] diff --git a/tests/chaos/chaos-mesh-rbac.yaml b/tests/chaos/chaos-mesh-rbac.yaml new file mode 100644 index 0000000..336ba42 --- /dev/null +++ b/tests/chaos/chaos-mesh-rbac.yaml @@ -0,0 +1,39 @@ +kind: ServiceAccount +apiVersion: v1 +metadata: + namespace: chaos-mesh + name: chaos-mesh-cluster-manager +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + namespace: vfps + name: chaos-mesh-cluster-manager +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: role-chaos-mesh-cluster-manager +rules: + - apiGroups: [""] + resources: ["pods", "namespaces"] + verbs: ["get", "watch", "list"] + - apiGroups: ["chaos-mesh.org"] + resources: ["*"] + verbs: ["get", "list", "watch", "create", "delete", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: bind-chaos-mesh-cluster-manager +subjects: + - kind: ServiceAccount + name: chaos-mesh-cluster-manager + namespace: chaos-mesh + - kind: ServiceAccount + name: chaos-mesh-cluster-manager + namespace: vfps +roleRef: + kind: ClusterRole + name: role-chaos-mesh-cluster-manager + apiGroup: rbac.authorization.k8s.io diff --git a/tests/chaos/chaos.yaml b/tests/chaos/chaos.yaml new file mode 100644 index 0000000..783f93e --- /dev/null +++ b/tests/chaos/chaos.yaml @@ -0,0 +1,21 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: Schedule +metadata: + namespace: vfps + name: fail-one-of-the-vfps-pods +spec: + schedule: "@every 1m" + concurrencyPolicy: Forbid + historyLimit: 1 + type: PodChaos + podChaos: + selector: + namespaces: + - vfps + labelSelectors: + app.kubernetes.io/name: vfps + app.kubernetes.io/instance: vfps + mode: one + action: pod-failure + duration: 30s +# TODO: could add database/cnpg chaos if deployed HA. diff --git a/tests/chaos/vfps-values.yaml b/tests/chaos/vfps-values.yaml new file mode 100644 index 0000000..6bc803e --- /dev/null +++ b/tests/chaos/vfps-values.yaml @@ -0,0 +1 @@ +replicaCount: 3