From ad244f4355523588fcd431a6771571f82085d38b Mon Sep 17 00:00:00 2001 From: Jordan Hoeft Date: Tue, 6 Feb 2024 16:16:16 -0600 Subject: [PATCH] initialize repo (#1) --- .github/ct.yaml | 2 + .github/helm-docs.sh | 16 ++ .github/workflows/ci.yaml | 63 ++++++ .github/workflows/release.yaml | 43 ++++ .pre-commit-config.yaml | 7 + LICENSE.md | 202 ++++++++++++++++++ README.md | 19 +- charts/tsm-node/.helmignore | 23 ++ charts/tsm-node/Chart.yaml | 9 + charts/tsm-node/README.md | 52 +++++ charts/tsm-node/templates/NOTES.txt | 22 ++ charts/tsm-node/templates/_helpers.tpl | 62 ++++++ charts/tsm-node/templates/configmap.yaml | 9 + charts/tsm-node/templates/deployment.yaml | 94 ++++++++ charts/tsm-node/templates/ingress.yaml | 54 +++++ charts/tsm-node/templates/service-mpc.yaml | 25 +++ charts/tsm-node/templates/service-sdk.yaml | 16 ++ charts/tsm-node/templates/serviceaccount.yaml | 13 ++ charts/tsm-node/values.yaml | 101 +++++++++ examples/db-setup/README.md | 1 + examples/db-setup/db-setup0.yaml | 119 +++++++++++ examples/db-setup/db-setup1.yaml | 120 +++++++++++ examples/db-setup/db-setup2.yaml | 120 +++++++++++ examples/tsm-node-multiinstance/README.md | 30 +++ .../assets/tsm-cluster.jpeg | Bin 0 -> 21175 bytes examples/tsm-node-multiinstance/tsm0.yaml | 134 ++++++++++++ examples/tsm-node-multiinstance/tsm1.yaml | 134 ++++++++++++ examples/tsm-node-multiinstance/tsm2.yaml | 135 ++++++++++++ 28 files changed, 1624 insertions(+), 1 deletion(-) create mode 100644 .github/ct.yaml create mode 100755 .github/helm-docs.sh create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE.md create mode 100644 charts/tsm-node/.helmignore create mode 100644 charts/tsm-node/Chart.yaml create mode 100644 charts/tsm-node/README.md create mode 100644 charts/tsm-node/templates/NOTES.txt create mode 100644 charts/tsm-node/templates/_helpers.tpl create mode 100644 charts/tsm-node/templates/configmap.yaml create mode 100644 charts/tsm-node/templates/deployment.yaml create mode 100644 charts/tsm-node/templates/ingress.yaml create mode 100644 charts/tsm-node/templates/service-mpc.yaml create mode 100644 charts/tsm-node/templates/service-sdk.yaml create mode 100644 charts/tsm-node/templates/serviceaccount.yaml create mode 100644 charts/tsm-node/values.yaml create mode 100644 examples/db-setup/README.md create mode 100644 examples/db-setup/db-setup0.yaml create mode 100644 examples/db-setup/db-setup1.yaml create mode 100644 examples/db-setup/db-setup2.yaml create mode 100644 examples/tsm-node-multiinstance/README.md create mode 100644 examples/tsm-node-multiinstance/assets/tsm-cluster.jpeg create mode 100644 examples/tsm-node-multiinstance/tsm0.yaml create mode 100644 examples/tsm-node-multiinstance/tsm1.yaml create mode 100644 examples/tsm-node-multiinstance/tsm2.yaml diff --git a/.github/ct.yaml b/.github/ct.yaml new file mode 100644 index 0000000..2ede44b --- /dev/null +++ b/.github/ct.yaml @@ -0,0 +1,2 @@ +debug: true +target-branch: main diff --git a/.github/helm-docs.sh b/.github/helm-docs.sh new file mode 100755 index 0000000..ddaa7ba --- /dev/null +++ b/.github/helm-docs.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +mkdir ./.bin +export PATH="./.bin:$PATH" + +set -euxo pipefail + +HELM_DOCS_VERSION=1.12.0 + +# install helm-docs +curl --silent --show-error --fail --location --output /tmp/helm-docs.tar.gz https://github.com/norwoodj/helm-docs/releases/download/v"${HELM_DOCS_VERSION}"/helm-docs_"${HELM_DOCS_VERSION}"_Linux_x86_64.tar.gz +tar -C .bin/ -xf /tmp/helm-docs.tar.gz helm-docs + +# validate docs +helm-docs +git diff --exit-code \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..432fa50 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,63 @@ +name: Lint and Test Charts + +on: + push: + branches: + - main + pull_request: + + merge_group: + +jobs: + lint-chart: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: v3.12.1 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + check-latest: true + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.6.0 + + - name: Run lint + run: ct lint --config .github/ct.yaml + + lint-docs: + runs-on: self-hosted + needs: lint-chart + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run helm-docs + run: .github/helm-docs.sh + + # Catch-all required check for test matrix + test-success: + needs: + - lint-chart + - lint-docs + runs-on: self-hosted + timeout-minutes: 1 + if: always() + steps: + - name: Fail for failed or cancelled lint-chart + if: | + needs.lint-chart.result == 'failure' || + needs.lint-chart.result == 'cancelled' + run: exit 1 + - name: Fail for failed or cancelled lint-docs + if: | + needs.lint-docs.result == 'failure' || + needs.lint-docs.result == 'cancelled' + run: exit 1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..4fe9538 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,43 @@ +name: Release Charts + +on: + push: + branches: + - main + paths: + - "charts/**" + workflow_dispatch: + +jobs: + release: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.12.1 + + - name: Build chart dependencies + run: | + for dir in charts/*/ + do + (cd ${dir}; helm dependency build) + done + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + with: + version: v1.6.1 + env: + CR_TOKEN: "${{ secrets.CR_TOKEN }}" + CR_GENERATE_RELEASE_NOTES: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0c29f3e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/norwoodj/helm-docs + rev: v1.12.0 + hooks: + - id: helm-docs + args: + - --chart-search-root=charts diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..84964b1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2024 Blockdaemon + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 8eeaf1b..f391593 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -# builder-vault-helm \ No newline at end of file +# TSM Builder Vault Helm Charts Repository + +Welcome to the repository containing Helm charts for deploying TSM Builder Vault services on Kubernetes. + +## Helm Repository + +``` +helm repo add builder-vault https://blockdaemon.github.io/builder-vault-helm/ +helm repo update +``` + +## TSM Node Chart + +For deploying a TSM node, you can find the specific Helm chart and its documentation in the `charts/tsm-node` directory. The README in that directory provides detailed information on how to configure and install the TSM node chart. + +Please refer to the following README for more information: + +[charts/tsm-node/README.md](charts/tsm-node/README.md) diff --git a/charts/tsm-node/.helmignore b/charts/tsm-node/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/tsm-node/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/tsm-node/Chart.yaml b/charts/tsm-node/Chart.yaml new file mode 100644 index 0000000..e0e75e2 --- /dev/null +++ b/charts/tsm-node/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: tsm-node +description: A Helm chart to deploy a Blockdaemon TSM node to kubernetes +maintainers: + - name: Blockdaemon + email: sre@blockdaemon.com +type: application +version: 0.1.0 +appVersion: "61.0.2" diff --git a/charts/tsm-node/README.md b/charts/tsm-node/README.md new file mode 100644 index 0000000..4c285e9 --- /dev/null +++ b/charts/tsm-node/README.md @@ -0,0 +1,52 @@ +# tsm-node + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 61.0.2](https://img.shields.io/badge/AppVersion-61.0.2-informational?style=flat-square) + +A Helm chart to deploy a Blockdaemon TSM node to kubernetes + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Blockdaemon | | | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| config.configFile | string | `""` | the TSM configuration file that will be mounted into the TSM node. MUTUALLY EXCLUSIVE with configSecretName | +| config.configSecretName | string | `""` | The name of the secret containing the TSM configuration file. MUTUALLY EXCLUSIVE with configFile | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `""` | Image to use for deploying the TSM node | +| image.tag | string | `""` | | +| imagePullSecrets | list | `[]` | | +| index | int | `0` | | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `""` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"chart-example.local"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"Prefix"` | | +| ingress.tls | list | `[]` | | +| mpcService | object | `{}` | Optional. Only used for flexibility to expose the mpc port outside of the cluster. | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| sdkService | object | `{"type":"ClusterIP"}` | The primary service definition for the TSM node | +| securityContext | object | `{}` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.automount | bool | `true` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | +| tolerations | list | `[]` | | +| volumeMounts | list | `[]` | Additional volumeMounts on the output Deployment definition. | +| volumes | list | `[]` | Additional volumes on the output Deployment definition. | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.12.0](https://github.com/norwoodj/helm-docs/releases/v1.12.0) diff --git a/charts/tsm-node/templates/NOTES.txt b/charts/tsm-node/templates/NOTES.txt new file mode 100644 index 0000000..3bf77ee --- /dev/null +++ b/charts/tsm-node/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.sdkService.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "tsm-node.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.sdkService.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "tsm-node.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "tsm-node.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.sdkService.port }} +{{- else if contains "ClusterIP" .Values.sdkService.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "tsm-node.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/tsm-node/templates/_helpers.tpl b/charts/tsm-node/templates/_helpers.tpl new file mode 100644 index 0000000..54103f2 --- /dev/null +++ b/charts/tsm-node/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "tsm-node.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "tsm-node.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "tsm-node.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "tsm-node.labels" -}} +helm.sh/chart: {{ include "tsm-node.chart" . }} +{{ include "tsm-node.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "tsm-node.selectorLabels" -}} +app.kubernetes.io/name: {{ include "tsm-node.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "tsm-node.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "tsm-node.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/tsm-node/templates/configmap.yaml b/charts/tsm-node/templates/configmap.yaml new file mode 100644 index 0000000..eb3da80 --- /dev/null +++ b/charts/tsm-node/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "tsm-node.fullname" . }} + labels: + {{- include "tsm-node.labels" . | nindent 4 }} +data: + config.toml: | + {{- .Values.config.configFile | nindent 4 }} \ No newline at end of file diff --git a/charts/tsm-node/templates/deployment.yaml b/charts/tsm-node/templates/deployment.yaml new file mode 100644 index 0000000..0556084 --- /dev/null +++ b/charts/tsm-node/templates/deployment.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "tsm-node.fullname" . }} + labels: + {{- include "tsm-node.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "tsm-node.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "tsm-node.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "tsm-node.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + {{- range .Values.sdkService.ports }} + - containerPort: {{ .targetPort }} + name: {{ .name }} + protocol: TCP + {{- end }} + {{- range .Values.mpcService.ports }} + - containerPort: {{ .targetPort }} + name: {{ .name }} + protocol: TCP + {{- end }} + livenessProbe: #TODO + httpGet: + path: /ping + port: sdk + readinessProbe: + httpGet: + path: /ping + port: sdk + resources: + {{- toYaml .Values.resources | nindent 12 }} + + volumeMounts: + - name: config-volume + mountPath: /config + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + + volumes: + - name: config-volume + {{- if and .Values.config.configFile .Values.config.configSecretName }} + {{- fail "config.configFile and config.configSecretName are mutually exclusive" }} + {{- else if .Values.config.configFile }} + configMap: + name: {{ template "tsm-node.fullname" . }} + {{- else if .Values.config.configSecretName }} + secret: + secretName: {{ .Values.config.configSecretName }} + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/tsm-node/templates/ingress.yaml b/charts/tsm-node/templates/ingress.yaml new file mode 100644 index 0000000..587e6ce --- /dev/null +++ b/charts/tsm-node/templates/ingress.yaml @@ -0,0 +1,54 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "tsm-node.fullname" . -}} +{{- $svcPort := .Values.sdkService.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "tsm-node.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path | quote }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ .port }} + {{- end -}} + {{- end }} +{{- end }} diff --git a/charts/tsm-node/templates/service-mpc.yaml b/charts/tsm-node/templates/service-mpc.yaml new file mode 100644 index 0000000..2461366 --- /dev/null +++ b/charts/tsm-node/templates/service-mpc.yaml @@ -0,0 +1,25 @@ +{{- if .Values.mpcService.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "tsm-node.fullname" . }}-mpc + labels: + {{- include "tsm-node.labels" . | nindent 4 }} + {{- with .Values.mpcService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.mpcService.loadBalancerClass}} + loadBalancerClass: {{ .Values.mpcService.loadBalancerClass }} + {{- end }} + type: {{ .Values.mpcService.type }} + ports: + {{- range .Values.mpcService.ports }} + - port: {{ .port }} + name: {{ .name }} + protocol: TCP + {{- end }} + selector: + {{- include "tsm-node.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/tsm-node/templates/service-sdk.yaml b/charts/tsm-node/templates/service-sdk.yaml new file mode 100644 index 0000000..0ab4ece --- /dev/null +++ b/charts/tsm-node/templates/service-sdk.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "tsm-node.fullname" . }} + labels: + {{- include "tsm-node.labels" . | nindent 4 }} +spec: + type: {{ .Values.sdkService.type }} + ports: + {{- range .Values.sdkService.ports }} + - port: {{ .port }} + name: {{ .name }} + protocol: TCP + {{- end }} + selector: + {{- include "tsm-node.selectorLabels" . | nindent 4 }} diff --git a/charts/tsm-node/templates/serviceaccount.yaml b/charts/tsm-node/templates/serviceaccount.yaml new file mode 100644 index 0000000..f72abf0 --- /dev/null +++ b/charts/tsm-node/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "tsm-node.serviceAccountName" . }} + labels: + {{- include "tsm-node.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/tsm-node/values.yaml b/charts/tsm-node/values.yaml new file mode 100644 index 0000000..138f071 --- /dev/null +++ b/charts/tsm-node/values.yaml @@ -0,0 +1,101 @@ +# Default values for tsm-node. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 +index: 0 + +config: + # -- the TSM configuration file that will be mounted into the TSM node. MUTUALLY EXCLUSIVE with configSecretName + configFile: "" + # -- The name of the secret containing the TSM configuration file. MUTUALLY EXCLUSIVE with configFile + configSecretName: "" + +image: + # -- Image to use for deploying the TSM node + repository: "" + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# -- The primary service definition for the TSM node +sdkService: + type: ClusterIP + +# -- Optional. Only used for flexibility to expose the mpc port outside of the cluster. +mpcService: {} + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# -- Additional volumes on the output Deployment definition. +volumes: [] + +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# -- Additional volumeMounts on the output Deployment definition. +volumeMounts: [] + +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/examples/db-setup/README.md b/examples/db-setup/README.md new file mode 100644 index 0000000..83b8f0f --- /dev/null +++ b/examples/db-setup/README.md @@ -0,0 +1 @@ +This directory contains an example of how you can bootstrap a postgres RDS for use with TSM. \ No newline at end of file diff --git a/examples/db-setup/db-setup0.yaml b/examples/db-setup/db-setup0.yaml new file mode 100644 index 0000000..2a18cb8 --- /dev/null +++ b/examples/db-setup/db-setup0.yaml @@ -0,0 +1,119 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: db-setup-script +data: + db-setup.sh: | + #!/bin/bash + + apt-get update && apt-get install -y jq python3 python3-pip + + pip3 install awscli --break-system-packages + + # Check if the correct number of arguments is given + if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Assign the argument to a variable + USERNAME=$1 + SCHEMA_NAME=$1 + + export AWS_DEFAULT_REGION="us-east-1" + + # Name of the secret in AWS Secrets Manager for initial RDS credentials + INITIAL_SECRET_NAME="tsm-db-secret-0" + + # Name of the new secret for the user's credentials + NEW_SECRET_NAME="tsm-db-secret-0-$USERNAME" + + # Use AWS CLI to retrieve the initial secret value + INITIAL_SECRET_VALUE=$(aws secretsmanager get-secret-value --secret-id $INITIAL_SECRET_NAME --query SecretString --output text) + + # Exit if the initial secret cannot be retrieved + if [ $? -ne 0 ]; then + echo "Failed to retrieve initial secret from AWS Secrets Manager." + exit 1 + fi + + # Parse the JSON secret value to extract the RDS credentials + RDS_ENDPOINT=$(echo $INITIAL_SECRET_VALUE | jq -r '.host') + RDS_PORT=$(echo $INITIAL_SECRET_VALUE | jq -r '.port') + MASTER_USERNAME=$(echo $INITIAL_SECRET_VALUE | jq -r '.username') + MASTER_PASSWORD=$(echo $INITIAL_SECRET_VALUE | jq -r '.password') + + # Generate a random password for the new user + PASSWORD=$(openssl rand -base64 12) + + # Connect to the RDS database and execute the SQL commands + PGPASSWORD=$MASTER_PASSWORD psql -h $RDS_ENDPOINT -p $RDS_PORT -U $MASTER_USERNAME < diff --git a/examples/db-setup/db-setup1.yaml b/examples/db-setup/db-setup1.yaml new file mode 100644 index 0000000..69bb101 --- /dev/null +++ b/examples/db-setup/db-setup1.yaml @@ -0,0 +1,120 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: db-setup-script +data: + db-setup.sh: | + #!/bin/bash + + apt-get update && apt-get install -y jq python3 python3-pip + + pip3 install awscli --break-system-packages + + # Check if the correct number of arguments is given + if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Assign the argument to a variable + USERNAME=$1 + SCHEMA_NAME=$1 + + export AWS_DEFAULT_REGION="us-east-1" + + # Name of the secret in AWS Secrets Manager for initial RDS credentials + INITIAL_SECRET_NAME="tsm-db-secret-1" + + # Name of the new secret for the user's credentials + NEW_SECRET_NAME="tsm-db-secret-1-$USERNAME" + + # Use AWS CLI to retrieve the initial secret value + INITIAL_SECRET_VALUE=$(aws secretsmanager get-secret-value --secret-id $INITIAL_SECRET_NAME --query SecretString --output text) + + # Exit if the initial secret cannot be retrieved + if [ $? -ne 0 ]; then + echo "Failed to retrieve initial secret from AWS Secrets Manager." + exit 1 + fi + + # Parse the JSON secret value to extract the RDS credentials + RDS_ENDPOINT=$(echo $INITIAL_SECRET_VALUE | jq -r '.host') + RDS_PORT=$(echo $INITIAL_SECRET_VALUE | jq -r '.port') + MASTER_USERNAME=$(echo $INITIAL_SECRET_VALUE | jq -r '.username') + MASTER_PASSWORD=$(echo $INITIAL_SECRET_VALUE | jq -r '.password') + + # Generate a random password for the new user + PASSWORD=$(openssl rand -base64 12) + + # Connect to the RDS database and execute the SQL commands + PGPASSWORD=$MASTER_PASSWORD psql -h $RDS_ENDPOINT -p $RDS_PORT -U $MASTER_USERNAME < diff --git a/examples/db-setup/db-setup2.yaml b/examples/db-setup/db-setup2.yaml new file mode 100644 index 0000000..dcb1aa5 --- /dev/null +++ b/examples/db-setup/db-setup2.yaml @@ -0,0 +1,120 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: db-setup-script +data: + db-setup.sh: | + #!/bin/bash + + apt-get update && apt-get install -y jq python3 python3-pip + + pip3 install awscli --break-system-packages + + # Check if the correct number of arguments is given + if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Assign the argument to a variable + USERNAME=$1 + SCHEMA_NAME=$1 + + export AWS_DEFAULT_REGION="us-east-1" + + # Name of the secret in AWS Secrets Manager for initial RDS credentials + INITIAL_SECRET_NAME="tsm-db-secret-2" + + # Name of the new secret for the user's credentials + NEW_SECRET_NAME="tsm-db-secret-2-$USERNAME" + + # Use AWS CLI to retrieve the initial secret value + INITIAL_SECRET_VALUE=$(aws secretsmanager get-secret-value --secret-id $INITIAL_SECRET_NAME --query SecretString --output text) + + # Exit if the initial secret cannot be retrieved + if [ $? -ne 0 ]; then + echo "Failed to retrieve initial secret from AWS Secrets Manager." + exit 1 + fi + + # Parse the JSON secret value to extract the RDS credentials + RDS_ENDPOINT=$(echo $INITIAL_SECRET_VALUE | jq -r '.host') + RDS_PORT=$(echo $INITIAL_SECRET_VALUE | jq -r '.port') + MASTER_USERNAME=$(echo $INITIAL_SECRET_VALUE | jq -r '.username') + MASTER_PASSWORD=$(echo $INITIAL_SECRET_VALUE | jq -r '.password') + + # Generate a random password for the new user + PASSWORD=$(openssl rand -base64 12) + + # Connect to the RDS database and execute the SQL commands + PGPASSWORD=$MASTER_PASSWORD psql -h $RDS_ENDPOINT -p $RDS_PORT -U $MASTER_USERNAME < diff --git a/examples/tsm-node-multiinstance/README.md b/examples/tsm-node-multiinstance/README.md new file mode 100644 index 0000000..6375904 --- /dev/null +++ b/examples/tsm-node-multiinstance/README.md @@ -0,0 +1,30 @@ +# TSM Multi Instance with tsm-node chart + +This directory has an example of deploying a multi instance TSM cluster with the tsm-node helm chart to a single kubernetes cluster. + +Full documentation of the configuration can be found [here](https://builder-vault-tsm.docs.blockdaemon.com/docs/example-tsm-configuration-file). + +## Helm Repository + +``` +helm repo add builder-vault https://blockdaemon.github.io/builder-vault-helm/ +helm repo update +``` + +## Prerequisites + - An EKS cluster deployed with the [AWS Loadbalancer Controller](https://docs.aws.amazon.com/eks/latest/userguide/aws-load-balancer-controller.html) installed and configured. + +The cluster deployed will look like this: + +![TSM Cluster](assets/tsm-cluster.jpeg) + + +The values files (tsm<0-2>.yaml) have example configurations for deploying each node in a way that they can communicate with each other and provision ingress to the SDK port. + +To deploy, you would perform 3 helm deployments: +``` +helm install tsm0 blockdaemon/tsm-node --create-namespace -n tsm -f tsm0.yaml +helm install tsm1 blockdaemon/tsm-node --create-namespace -n tsm -f tsm1.yaml +helm install tsm2 blockdaemon/tsm-node --create-namespace -n tsm -f tsm2.yaml +``` + diff --git a/examples/tsm-node-multiinstance/assets/tsm-cluster.jpeg b/examples/tsm-node-multiinstance/assets/tsm-cluster.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8af4e82ce3541fb1a33cf1e71ba2b886e122314f GIT binary patch literal 21175 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 C$R0cZ literal 0 HcmV?d00001 diff --git a/examples/tsm-node-multiinstance/tsm0.yaml b/examples/tsm-node-multiinstance/tsm0.yaml new file mode 100644 index 0000000..93425c7 --- /dev/null +++ b/examples/tsm-node-multiinstance/tsm0.yaml @@ -0,0 +1,134 @@ +replicaCount: 2 +index: 0 + +config: + # https://builder-vault-tsm.docs.blockdaemon.com/docs/example-tsm-configuration-file + configFile: | + [MPC] + Threshold = 1 + PlayerCount = 3 + + [Player] + Index = 0 + # This is a base64 encoding of the private key used to authenticate the local player towards the remote players. This + # must correspond to the public keys configured on the remote players for this player index. A private key can be + # generated using the following OpenSSL commands: + # + # openssl ecparam -name P-256 -genkey -param_enc named_curve -outform DER -out private.key + # openssl base64 -A -in private.key; echo + # + # Instead of P-256 one can use P-384 or P-521 depending on the desired security level (128, 192 or 256 bits). + PrivateKey = "BA3E64==" + + [Players.1] + Address = "tsm1-tsm-node:9000" + # This is a base64 encoding of the players public key. A public key can be generated from the private key using the + # following OpenSSL commands: + # + # openssl ec -inform DER -in private.key -pubout -outform DER -out public.key + # openssl base64 -A -in public.key; echo + PublicKey = "BA3E64==" + + [Players.2] + Address = "tsm2-tsm-node:9000" + PublicKey = "BA3E64==" + + [Authentication] + # List of API keys used for authentication in SDKv2 + [[Authentication.APIKeys]] + # Only for SDK V2 + # Base64 encoded hash of the API key. A hash for the API key foobar can be generated with the following command: + # + # echo -n "foobar" | openssl dgst -sha256 -binary | openssl base64 + #APIKey = "" + # Users with the given API key will be mapped to this user in the system. If the user does not exist, it will be + # created automatically. Set this to an existing user ID to migrate from password to API key authentication. + #ApplicationID = "" + + [Database] + DriverName = "postgres" + # This specifies a master encryption key used to protect database records. Note that this key is not directly + # used to encrypt data. Use any long random string here and make sure to keep a backup of it somewhere safe. + EncryptorMasterPassword = "ENCRYPTION_KEY" + DataSourceName = "host=.rds.amazonaws.com port=5432 user=tsm0 password=mypass dbname=tsm0 sslmode=require" + + [MPCTCPServer] + Port = 9000 + + [SDKServer] + Port = 8080 + + [SEPD19S] + EnableShareBackup = true + EnableERSExport = true + [DKLS19] + EnableShareBackup = true + EnableERSExport = true + + [MultiInstance] + CleanupInterval = "5m" + CleanupProbability = 75 + + [Audit] + # URL of the audit receiver. Audit logs are sent to this URL + # Can be a file, HTTP location or s3 location: file://, https://, s3:// + #ReceiverURL = "" + + # When using an S3-compatible API as the ReceiverURL in [Audit], specify any + # non-standard S3 related parameters here + [Audit.S3EndpointConfig] + # If not using the default S3 endpoint, specify the custom one here + #EndpointURL = "" + # AWS or S3-compatible API region + #Region = "" + # Authorization keys for the S3-compatible API + #SecretAccessKey = "" + #AccessKeyId = "" + #SessionToken = "" + +image: + repository: + pullPolicy: IfNotPresent + tag: "61.0.2" # override the version of the image + +sdkService: + type: NodePort + ports: + - port: 8080 + name: sdk + targetPort: 8080 + - port: 9000 + name: mpc + targetPort: 9000 + +mpcService: + enabled: false + +ingress: + enabled: true + className: "alb" + annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/certificate-arn: + alb.ingress.kubernetes.io/healthcheck-path: /ping + hosts: + - host: "tsm0-sdk.exmaple.com" + paths: + - path: / + pathType: Prefix + port: 8080 + +affinity: + podAntiAffinity: # spread the pods across nodes + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - tsm-node + topologyKey: kubernetes.io/hostname + +resources: + requests: + cpu: 14 diff --git a/examples/tsm-node-multiinstance/tsm1.yaml b/examples/tsm-node-multiinstance/tsm1.yaml new file mode 100644 index 0000000..ffcc10d --- /dev/null +++ b/examples/tsm-node-multiinstance/tsm1.yaml @@ -0,0 +1,134 @@ +replicaCount: 3 +index: 1 + +config: + # https://builder-vault-tsm.docs.blockdaemon.com/docs/example-tsm-configuration-file + configFile: | + [MPC] + Threshold = 1 + PlayerCount = 3 + + [Player] + Index = 1 + # This is a base64 encoding of the private key used to authenticate the local player towards the remote players. This + # must correspond to the public keys configured on the remote players for this player index. A private key can be + # generated using the following OpenSSL commands: + # + # openssl ecparam -name P-256 -genkey -param_enc named_curve -outform DER -out private.key + # openssl base64 -A -in private.key; echo + # + # Instead of P-256 one can use P-384 or P-521 depending on the desired security level (128, 192 or 256 bits). + PrivateKey = "BA3E64==" + + [Players.0] + Address = "tsm0-tsm-node:9000" + # This is a base64 encoding of the players public key. A public key can be generated from the private key using the + # following OpenSSL commands: + # + # openssl ec -inform DER -in private.key -pubout -outform DER -out public.key + # openssl base64 -A -in public.key; echo + PublicKey = "BA3E64==" + + [Players.2] + Address = "tsm2-tsm-node:9000" + PublicKey = "BA3E64==" + + [Authentication] + # List of API keys used for authentication in SDKv2 + [[Authentication.APIKeys]] + # Only for SDK V2 + # Base64 encoded hash of the API key. A hash for the API key foobar can be generated with the following command: + # + # echo -n "foobar" | openssl dgst -sha256 -binary | openssl base64 + #APIKey = "" + # Users with the given API key will be mapped to this user in the system. If the user does not exist, it will be + # created automatically. Set this to an existing user ID to migrate from password to API key authentication. + #ApplicationID = "" + + [Database] + DriverName = "postgres" + # This specifies a master encryption key used to protect database records. Note that this key is not directly + # used to encrypt data. Use any long random string here and make sure to keep a backup of it somewhere safe. + EncryptorMasterPassword = "ENCRYPTION_KEY" + DataSourceName = "host=.rds.amazonaws.com port=5432 user=tsm1 password=mypass dbname=tsm1 sslmode=require" + + [MPCTCPServer] + Port = 9000 + + [SDKServer] + Port = 8080 + + [SEPD19S] + EnableShareBackup = true + EnableERSExport = true + [DKLS19] + EnableShareBackup = true + EnableERSExport = true + + [MultiInstance] + CleanupInterval = "5m" + CleanupProbability = 75 + + [Audit] + # URL of the audit receiver. Audit logs are sent to this URL + # Can be a file, HTTP location or s3 location: file://, https://, s3:// + #ReceiverURL = "" + + # When using an S3-compatible API as the ReceiverURL in [Audit], specify any + # non-standard S3 related parameters here + [Audit.S3EndpointConfig] + # If not using the default S3 endpoint, specify the custom one here + #EndpointURL = "" + # AWS or S3-compatible API region + #Region = "" + # Authorization keys for the S3-compatible API + #SecretAccessKey = "" + #AccessKeyId = "" + #SessionToken = "" + +image: + repository: + pullPolicy: IfNotPresent + tag: "61.0.2" # override the version of the image + +sdkService: + type: NodePort + ports: + - port: 8080 + name: sdk + targetPort: 8080 + - port: 9000 + name: mpc + targetPort: 9000 + +mpcService: + enabled: false + +ingress: + enabled: true + className: "alb" + annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/certificate-arn: + alb.ingress.kubernetes.io/healthcheck-path: /ping + hosts: + - host: "tsm1-sdk.exmaple.com" + paths: + - path: / + pathType: Prefix + port: 8080 + +affinity: + podAntiAffinity: # spread the pods across nodes + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - tsm-node + topologyKey: kubernetes.io/hostname + +resources: + requests: + cpu: 14 diff --git a/examples/tsm-node-multiinstance/tsm2.yaml b/examples/tsm-node-multiinstance/tsm2.yaml new file mode 100644 index 0000000..a7b18e1 --- /dev/null +++ b/examples/tsm-node-multiinstance/tsm2.yaml @@ -0,0 +1,135 @@ +replicaCount: 1 +index: 2 + +config: + # https://builder-vault-tsm.docs.blockdaemon.com/docs/example-tsm-configuration-file + configFile: | + [MPC] + Threshold = 1 + PlayerCount = 3 + + [Player] + Index = 2 + # This is a base64 encoding of the private key used to authenticate the local player towards the remote players. This + # must correspond to the public keys configured on the remote players for this player index. A private key can be + # generated using the following OpenSSL commands: + # + # openssl ecparam -name P-256 -genkey -param_enc named_curve -outform DER -out private.key + # openssl base64 -A -in private.key; echo + # + # Instead of P-256 one can use P-384 or P-521 depending on the desired security level (128, 192 or 256 bits). + PrivateKey = "BA3E64==" + + [Players.0] + Address = "tsm1-tsm-node:9000" + # This is a base64 encoding of the players public key. A public key can be generated from the private key using the + # following OpenSSL commands: + # + # openssl ec -inform DER -in private.key -pubout -outform DER -out public.key + # openssl base64 -A -in public.key; echo + PublicKey = "BA3E64==" + + [Players.1] + Address = "tsm2-tsm-node:9000" + PublicKey = "BA3E64==" + + [Authentication] + # List of API keys used for authentication in SDKv2 + [[Authentication.APIKeys]] + # Only for SDK V2 + # Base64 encoded hash of the API key. A hash for the API key foobar can be generated with the following command: + # + # echo -n "foobar" | openssl dgst -sha256 -binary | openssl base64 + #APIKey = "" + # Users with the given API key will be mapped to this user in the system. If the user does not exist, it will be + # created automatically. Set this to an existing user ID to migrate from password to API key authentication. + #ApplicationID = "" + + [Database] + DriverName = "postgres" + # This specifies a master encryption key used to protect database records. Note that this key is not directly + # used to encrypt data. Use any long random string here and make sure to keep a backup of it somewhere safe. + EncryptorMasterPassword = "ENCRYPTION_KEY" + DataSourceName = "host=.rds.amazonaws.com port=5432 user=tsm2 password=mypass dbname=tsm2 sslmode=require" + + [MPCTCPServer] + Port = 9000 + + [SDKServer] + Port = 8080 + + [SEPD19S] + EnableShareBackup = true + EnableERSExport = true + [DKLS19] + EnableShareBackup = true + EnableERSExport = true + + [MultiInstance] + CleanupInterval = "5m" + CleanupProbability = 75 + + [Audit] + # URL of the audit receiver. Audit logs are sent to this URL + # Can be a file, HTTP location or s3 location: file://, https://, s3:// + #ReceiverURL = "" + + # When using an S3-compatible API as the ReceiverURL in [Audit], specify any + # non-standard S3 related parameters here + [Audit.S3EndpointConfig] + # If not using the default S3 endpoint, specify the custom one here + #EndpointURL = "" + # AWS or S3-compatible API region + #Region = "" + # Authorization keys for the S3-compatible API + #SecretAccessKey = "" + #AccessKeyId = "" + #SessionToken = "" + +image: + repository: + pullPolicy: IfNotPresent + tag: "61.0.2" # override the version of the image + +sdkService: + type: NodePort + ports: + - port: 8080 + name: sdk + targetPort: 8080 + - port: 9000 + name: mpc + targetPort: 9000 + +mpcService: + enabled: false + type: LoadBalancer + +ingress: + enabled: true + className: "alb" + annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/certificate-arn: + alb.ingress.kubernetes.io/healthcheck-path: /ping + hosts: + - host: "tsm2-sdk.exmaple.com" + paths: + - path: / + pathType: Prefix + port: 8080 + +affinity: + podAntiAffinity: # spread the pods across nodes + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - tsm-node + topologyKey: kubernetes.io/hostname + +resources: + requests: + cpu: 14