Restructure Repository
This commit is contained in:
parent
5acedd795d
commit
2e2beb286d
|
@ -19,8 +19,8 @@ Useful tools for administration:
|
|||
https://docs.stackspin.net/en/v2/system_administration/customizing.html
|
||||
|
||||
### Guide: Creating OAuth Credentials for an external service
|
||||
- add a line in `basic/install.sh` and run it to generate the secret (TODO: Update to new stackspin mechanism)
|
||||
- append another OAuth2Client definition to `basic/overrides/oauth-clients.yaml`,
|
||||
- add a line in `install.sh` and run it to generate the secret (TODO: Update to new stackspin mechanism)
|
||||
- append another OAuth2Client definition to `overrides/oauth-clients.yaml`,
|
||||
adjusting `metadata.name` and `spec.secretName` as well as `spec.redirectUris`
|
||||
- apply changes to the cluster
|
||||
- obtain the generated `client_secret` for your application from kubernetes:
|
||||
|
@ -71,7 +71,7 @@ First [install Stackspin](https://docs.stackspin.net/en/latest/installation/inst
|
|||
Then apply the configuration to your cluster:
|
||||
|
||||
```sh
|
||||
basic/install.sh
|
||||
install.sh
|
||||
```
|
||||
|
||||
Done!
|
||||
|
|
|
@ -2,7 +2,7 @@ apiVersion: hydra.ory.sh/v1alpha1
|
|||
kind: OAuth2Client
|
||||
metadata:
|
||||
name: gitea-oauth-client
|
||||
# Has to live in the same namespace as the stackspin-wordpress-oauth-variables secret
|
||||
# Has to live in the same namespace as the stackspin-*-oauth-variables secret
|
||||
namespace: flux-system
|
||||
spec:
|
||||
# TODO copied from wekan: https://github.com/wekan/wekan/wiki/Keycloak
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: tcp-services
|
||||
namespace: stackspout
|
||||
data:
|
||||
22: "gitea:22"
|
|
@ -2,7 +2,7 @@ apiVersion: hydra.ory.sh/v1alpha1
|
|||
kind: OAuth2Client
|
||||
metadata:
|
||||
name: vikunja-test-oauth-client
|
||||
# Has to live in the same namespace as the stackspin-wordpress-oauth-variables secret
|
||||
# Has to live in the same namespace as the stackspin-*-oauth-variables secret
|
||||
namespace: flux-system
|
||||
spec:
|
||||
# TODO copied from wekan: https://github.com/wekan/wekan/wiki/Keycloak
|
|
@ -2,7 +2,7 @@ apiVersion: hydra.ory.sh/v1alpha1
|
|||
kind: OAuth2Client
|
||||
metadata:
|
||||
name: vikunja-oauth-client
|
||||
# Has to live in the same namespace as the stackspin-wordpress-oauth-variables secret
|
||||
# Has to live in the same namespace as the stackspin-*-oauth-variables secret
|
||||
namespace: flux-system
|
||||
spec:
|
||||
# TODO copied from wekan: https://github.com/wekan/wekan/wiki/Keycloak
|
|
@ -13,7 +13,7 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/apps/forge
|
||||
path: ./apps/forge
|
||||
prune: true
|
||||
postBuild:
|
||||
substituteFrom:
|
|
@ -13,7 +13,7 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/apps/code
|
||||
path: ./apps/code
|
||||
prune: true
|
||||
postBuild:
|
||||
substituteFrom:
|
|
@ -13,7 +13,7 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/apps/ninja
|
||||
path: ./apps/ninja
|
||||
prune: true
|
||||
postBuild:
|
||||
substituteFrom:
|
|
@ -22,9 +22,10 @@ data:
|
|||
stackspin.net/backupSet: "invoiceninja"
|
||||
podLabels:
|
||||
stackspin.net/backupSet: "invoiceninja"
|
||||
backup.velero.io/backup-volumes: "invoiceninja-data"
|
||||
persistence:
|
||||
public:
|
||||
existingClaim: invoiceninja-data
|
||||
existingClaim: "invoiceninja-data"
|
||||
mariadb:
|
||||
# https://github.com/bitnami/charts/blob/master/bitnami/mariadb/values.yaml
|
||||
commonLabels:
|
|
@ -14,7 +14,7 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/apps/do
|
||||
path: ./apps/do
|
||||
prune: true
|
||||
postBuild:
|
||||
substituteFrom:
|
|
@ -14,7 +14,7 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/apps/do-test
|
||||
path: ./apps/do-test
|
||||
prune: true
|
||||
postBuild:
|
||||
substituteFrom:
|
|
@ -1,246 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generates Kubernetes secrets based on a provided app name.
|
||||
|
||||
If the `templates` directory contains a secret called `stackspin-{app}-variables`, it
|
||||
will check if that secret already exists in the cluster, and if not: generate
|
||||
it. It does the same for an `stackspin-{app}-basic-auth` secret that will contain a
|
||||
password as well as a htpasswd encoded version of it.
|
||||
|
||||
See https://open.greenhost.net/stackspin/stackspin/-/issues/891 for the
|
||||
context why we use this script and not a helm chart to generate secrets.
|
||||
|
||||
usage: `python generate_secrets.py $appName`
|
||||
|
||||
As a special case, `python generate_secrets.py stackspin` will check that the
|
||||
`stackspin-cluster-variables` secret exists and that its values do not contain
|
||||
problematic characters.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import crypt
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
import sys
|
||||
|
||||
import jinja2
|
||||
import yaml
|
||||
from kubernetes import client, config
|
||||
from kubernetes.client import api_client
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
from kubernetes.utils import create_from_yaml
|
||||
from kubernetes.utils.create_from_yaml import FailToCreateError
|
||||
|
||||
# This script gets called with an app name as argument. Most of them need an
|
||||
# oauth client in Hydra, but some don't. This list contains the ones that
|
||||
# don't.
|
||||
APPS_WITHOUT_OAUTH = [
|
||||
"single-sign-on",
|
||||
"prometheus",
|
||||
"alertmanager",
|
||||
"suitecrm",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
"""Run everything."""
|
||||
# Add jinja filters we want to use
|
||||
env = jinja2.Environment(
|
||||
extensions=["jinja2_base64_filters.Base64Filters"])
|
||||
env.filters["generate_password"] = generate_password
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Please provide an app name as an argument")
|
||||
sys.exit(1)
|
||||
app_name = sys.argv[1]
|
||||
|
||||
if app_name == "stackspin":
|
||||
# This is a special case: we don't generate new secrets, but verify the
|
||||
# validity of the cluster variables (populated from .flux.env).
|
||||
verify_cluster_variables()
|
||||
else:
|
||||
# Create app variables secret
|
||||
create_variables_secret(
|
||||
app_name, f"stackspin-{app_name}-variables.yaml.jinja", env)
|
||||
# Create a secret that contains the oauth variables for Hydra Maester
|
||||
if app_name not in APPS_WITHOUT_OAUTH:
|
||||
create_variables_secret(
|
||||
app_name, "stackspin-oauth-variables.yaml.jinja", env)
|
||||
create_basic_auth_secret(app_name, env)
|
||||
|
||||
|
||||
def verify_cluster_variables():
|
||||
data = get_kubernetes_secret_data("stackspin-cluster-variables", "flux-system")
|
||||
if data is None:
|
||||
raise Exception("Secret stackspin-cluster-variables was not found.")
|
||||
message = "In secret stackspin-cluster-variables, key {}, the character {}" \
|
||||
" was used which will probably lead to problems, so aborting." \
|
||||
" You can update the value by using `kubectl edit secret -n" \
|
||||
" flux-system stackspin-cluster-variables`."
|
||||
for key, value in data.items():
|
||||
decoded_value = base64.b64decode(value).decode("ascii")
|
||||
for character in ["\"", "$"]:
|
||||
if character in decoded_value:
|
||||
raise Exception(message.format(key, character))
|
||||
|
||||
|
||||
def get_templates_dir():
|
||||
"""Returns directory that contains the Jinja templates used to create app secrets."""
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
|
||||
|
||||
|
||||
def create_variables_secret(app_name, variables_filename, env):
|
||||
"""Checks if a variables secret for app_name already exists, generates it if necessary."""
|
||||
variables_filepath = os.path.join(get_templates_dir(), variables_filename)
|
||||
if os.path.exists(variables_filepath):
|
||||
# Check if k8s secret already exists, if not, generate it
|
||||
with open(variables_filepath, encoding="UTF-8") as template_file:
|
||||
lines = template_file.read()
|
||||
secret_name, secret_namespace = get_secret_metadata(lines)
|
||||
new_secret_dict = yaml.safe_load(
|
||||
env.from_string(lines, globals={"app": app_name}).render()
|
||||
)
|
||||
current_secret_data = get_kubernetes_secret_data(
|
||||
secret_name, secret_namespace
|
||||
)
|
||||
if current_secret_data is None:
|
||||
# Create new secret
|
||||
update_secret = False
|
||||
elif current_secret_data.keys() != new_secret_dict["data"].keys():
|
||||
# Update current secret with new keys
|
||||
update_secret = True
|
||||
print(
|
||||
f"Secret {secret_name} in namespace {secret_namespace}"
|
||||
" already exists. Merging..."
|
||||
)
|
||||
# Merge dicts. Values from current_secret_data take precedence
|
||||
new_secret_dict["data"] |= current_secret_data
|
||||
else:
|
||||
# Do Nothing
|
||||
print(
|
||||
f"Secret {secret_name} in namespace {secret_namespace}"
|
||||
" is already in a good state, doing nothing."
|
||||
)
|
||||
return
|
||||
print(
|
||||
f"Storing secret {secret_name} in namespace"
|
||||
f" {secret_namespace} in cluster."
|
||||
)
|
||||
store_kubernetes_secret(
|
||||
new_secret_dict, secret_namespace, update=update_secret
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Template {variables_filename} does not exist, no action needed")
|
||||
|
||||
|
||||
def create_basic_auth_secret(app_name, env):
|
||||
"""Checks if a basic auth secret for app_name already exists, generates it if necessary."""
|
||||
basic_auth_filename = os.path.join(
|
||||
get_templates_dir(), f"stackspin-{app_name}-basic-auth.yaml.jinja"
|
||||
)
|
||||
if os.path.exists(basic_auth_filename):
|
||||
with open(basic_auth_filename, encoding="UTF-8") as template_file:
|
||||
lines = template_file.read()
|
||||
secret_name, secret_namespace = get_secret_metadata(lines)
|
||||
|
||||
if get_kubernetes_secret_data(secret_name, secret_namespace) is None:
|
||||
basic_auth_username = "admin"
|
||||
basic_auth_password = generate_password(32)
|
||||
basic_auth_htpasswd = gen_htpasswd(
|
||||
basic_auth_username, basic_auth_password
|
||||
)
|
||||
print(
|
||||
f"Adding secret {secret_name} in namespace"
|
||||
f" {secret_namespace} to cluster."
|
||||
)
|
||||
template = env.from_string(
|
||||
lines,
|
||||
globals={
|
||||
"pass": basic_auth_password,
|
||||
"htpasswd": basic_auth_htpasswd,
|
||||
},
|
||||
)
|
||||
secret_dict = yaml.safe_load(template.render())
|
||||
store_kubernetes_secret(secret_dict, secret_namespace)
|
||||
else:
|
||||
print(
|
||||
f"Secret {secret_name} in namespace {secret_namespace}"
|
||||
" already exists. Not generating new secrets."
|
||||
)
|
||||
else:
|
||||
print(f"File {basic_auth_filename} does not exist, no action needed")
|
||||
|
||||
|
||||
def get_secret_metadata(yaml_string):
|
||||
"""Returns secret name and namespace from metadata field in a yaml string."""
|
||||
secret_dict = yaml.safe_load(yaml_string)
|
||||
secret_name = secret_dict["metadata"]["name"]
|
||||
# default namespace is flux-system, but other namespace can be
|
||||
# provided in secret metadata
|
||||
if "namespace" in secret_dict["metadata"]:
|
||||
secret_namespace = secret_dict["metadata"]["namespace"]
|
||||
else:
|
||||
secret_namespace = "flux-system"
|
||||
return secret_name, secret_namespace
|
||||
|
||||
|
||||
def get_kubernetes_secret_data(secret_name, namespace):
|
||||
"""Returns the contents of a kubernetes secret or None if the secret does not exist."""
|
||||
try:
|
||||
secret = API.read_namespaced_secret(secret_name, namespace).data
|
||||
except ApiException as ex:
|
||||
# 404 is expected when the optional secret does not exist.
|
||||
if ex.status != 404:
|
||||
raise ex
|
||||
return None
|
||||
return secret
|
||||
|
||||
|
||||
def store_kubernetes_secret(secret_dict, namespace, update=False):
|
||||
"""Stores either a new secret in the cluster, or updates an existing one."""
|
||||
api_client_instance = api_client.ApiClient()
|
||||
if update:
|
||||
verb = "updated"
|
||||
api_response = patch_kubernetes_secret(secret_dict, namespace)
|
||||
else:
|
||||
verb = "created"
|
||||
try:
|
||||
api_response = create_from_yaml(
|
||||
api_client_instance,
|
||||
yaml_objects=[secret_dict],
|
||||
namespace=namespace
|
||||
)
|
||||
except FailToCreateError as ex:
|
||||
print(f"Secret not {verb} because of exception {ex}")
|
||||
return
|
||||
print(f"Secret {verb} with api response: {api_response}")
|
||||
|
||||
|
||||
def patch_kubernetes_secret(secret_dict, namespace):
|
||||
"""Patches secret in the cluster with new data."""
|
||||
api_client_instance = api_client.ApiClient()
|
||||
api_instance = client.CoreV1Api(api_client_instance)
|
||||
name = secret_dict["metadata"]["name"]
|
||||
body = {}
|
||||
body["data"] = secret_dict["data"]
|
||||
return api_instance.patch_namespaced_secret(name, namespace, body)
|
||||
|
||||
|
||||
def generate_password(length):
|
||||
"""Generates a password of "length" characters."""
|
||||
length = int(length)
|
||||
password = "".join((secrets.choice(string.ascii_letters)
|
||||
for i in range(length)))
|
||||
return password
|
||||
|
||||
|
||||
def gen_htpasswd(user, password):
|
||||
"""Generate htpasswd entry for user with password."""
|
||||
return f"{user}:{crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config.load_kube_config()
|
||||
API = client.CoreV1Api()
|
||||
main()
|
|
@ -0,0 +1,14 @@
|
|||
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: stackspout-apps
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
retryInterval: 1m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./apps
|
||||
prune: true
|
||||
validation: client
|
|
@ -9,6 +9,6 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/infrastructure/namespaces
|
||||
path: ./infrastructure/namespaces
|
||||
prune: true
|
||||
validation: client
|
|
@ -8,7 +8,7 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/overrides
|
||||
path: ./overrides
|
||||
prune: true
|
||||
validation: client
|
||||
postBuild:
|
|
@ -9,6 +9,6 @@ spec:
|
|||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: stackspout
|
||||
path: ./basic/infrastructure/sources
|
||||
path: ./infrastructure/sources
|
||||
prune: true
|
||||
validation: client
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: source.toolkit.fluxcd.io/v1beta1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: 8gears-n8n
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 60m
|
||||
url: oci://8gears.container-registry.com/library/n8n
|
|
@ -11,11 +11,6 @@ flux create source git stackspout \
|
|||
echo "Creating / Updating kustomization stackspout"
|
||||
flux create kustomization stackspout \
|
||||
--source=GitRepository/stackspout \
|
||||
--path="./basic/infrastructure/kustomizations/" \
|
||||
--path="./infrastructure/kustomizations/" \
|
||||
--prune=true \
|
||||
--interval=5m
|
||||
|
||||
python $(dirname "$0")/../generate_secrets.py vikunja
|
||||
python $(dirname "$0")/../generate_secrets.py vikunja-test
|
||||
python $(dirname "$0")/../generate_secrets.py gitea
|
||||
python $(dirname "$0")/../generate_secrets.py invoiceninja
|
|
@ -10,6 +10,8 @@ data:
|
|||
name: "Vikunja Tasks"
|
||||
gitea: |
|
||||
name: "Gitea Code"
|
||||
forgejo: |
|
||||
name: "Forgejo"
|
||||
invoiceninja: |
|
||||
name: "Invoiceninja Billing"
|
||||
---
|
||||
|
@ -24,3 +26,4 @@ metadata:
|
|||
data:
|
||||
vikunja: vikunja
|
||||
gitea: gitea
|
||||
forgejo: forgejo
|
|
@ -1,10 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: stackspin-invoiceninja-variables
|
||||
data:
|
||||
app_key: "{{ 32 | generate_password | b64encode }}"
|
||||
password: "{{ 32 | generate_password | b64encode }}"
|
||||
redis_password: "{{ 32 | generate_password | b64encode }}"
|
||||
mariadb_password: "{{ 32 | generate_password | b64encode }}"
|
||||
mariadb_root_password: "{{ 32 | generate_password | b64encode }}"
|
|
@ -1,9 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: stackspin-kimai-variables
|
||||
data:
|
||||
password: "{{ 32 | generate_password | b64encode }}"
|
||||
secret: "{{ 32 | generate_password | b64encode }}"
|
||||
mariadb_password: "{{ 32 | generate_password | b64encode }}"
|
||||
mariadb_root_password: "{{ 32 | generate_password | b64encode }}"
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: stackspin-{{ app }}-oauth-variables
|
||||
data:
|
||||
client_id: "{{ app | b64encode }}"
|
||||
client_secret: "{{ 32 | generate_password | b64encode }}"
|
|
@ -1,8 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: stackspin-suitecrm-variables
|
||||
data:
|
||||
password: "{{ 32 | generate_password | b64encode }}"
|
||||
mariadb_password: "{{ 32 | generate_password | b64encode }}"
|
||||
mariadb_root_password: "{{ 32 | generate_password | b64encode }}"
|
|
@ -1,7 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: stackspin-vikunja-variables
|
||||
data:
|
||||
jwt: "{{ 32 | generate_password | b64encode }}"
|
||||
postgresql_password: "{{ 32 | generate_password | b64encode }}"
|
Loading…
Reference in New Issue