Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • stackspin/stackspin-flux-example
  • xeruf/stackspout
2 results
Show changes
Showing
with 565 additions and 11 deletions
# For kimai2
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
name: robjuz
namespace: flux-system
spec:
interval: 60m
url: https://robjuz.github.io/helm-charts/
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
name: truecharts
namespace: flux-system
spec:
interval: 60m
url: https://charts.truecharts.org
#!/usr/bin/env bash
echo "Creating / updating gitRepository stackspin-flux-example-basic in namespace example-basic"
flux create source git stackspin-flux-example \
--namespace=example-basic \
--url=https://open.greenhost.net/stackspin/stackspin-flux-example.git \
kubectl get namespace stackspout 2>/dev/null || kubectl create namespace stackspout
echo "Creating / Updating gitRepository stackspout"
flux create source git stackspout \
--url=https://open.greenhost.net/xeruf/stackspout.git \
--branch=main \
--interval=1h
--interval=5m
echo "Creating / updating kustomization stackspin-flux-example in namespace example-basic"
flux create kustomization stackspin-flux-example \
--namespace=example-basic \
--source=GitRepository/stackspin-flux-example \
--path="./basic/clusters/production/" \
echo "Creating / Updating kustomization stackspout"
flux create kustomization stackspout \
--source=GitRepository/stackspout \
--path="./basic/infrastructure/kustomizations/" \
--prune=true \
--interval=1h
--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
#python $(dirname "$0")/../generate_secrets.py suitecrm
#python $(dirname "$0")/../generate_secrets.py kimai
python $(dirname "$0")/../generate_secrets.py wikijs
python $(dirname "$0")/../generate_secrets.py nextcloud-home
apiVersion: hydra.ory.sh/v1alpha1
kind: OAuth2Client
metadata:
name: nextcloud-home-oauth-client
namespace: flux-system
spec:
grantTypes:
- authorization_code
- refresh_token
- client_credentials
- implicit
responseTypes:
- id_token
- code
scope: "openid profile email stackspin_roles"
secretName: stackspin-nextcloud-home-oauth-variables
redirectUris:
- https://files.home.${domain}/index.php/apps/sociallogin/custom_oauth2/stackspin
- https://files.home.${domain}/apps/sociallogin/custom_oidc/stackspin
- https://files.home.${domain}/index.php/apps/sociallogin/custom_oidc/stackspin
- https://files.home.${domain}/apps/sociallogin/custom_oauth2/stackspin
tokenEndpointAuthMethod: client_secret_post
---
apiVersion: hydra.ory.sh/v1alpha1
kind: OAuth2Client
metadata:
name: wikijs-oauth-client
namespace: flux-system
spec:
grantTypes:
- authorization_code
- refresh_token
- client_credentials
- implicit
responseTypes:
- id_token
- code
scope: "openid profile email stackspin_roles"
secretName: stackspin-wikijs-oauth-variables
#redirectUris:
# - https://wiki.${domain}/user/oauth2/Stackspin/callback
tokenEndpointAuthMethod: client_secret_post
# This list of apps is loaded by the Stackspin dashboard on startup, and it
# will insert any missing entries into the Stackspin database (`apps` table).
apiVersion: v1
kind: ConfigMap
metadata:
name: stackspin-apps-custom
namespace: flux-system
data:
vikunja: |
name: "Vikunja Tasks"
gitea: |
name: "Gitea Code"
invoiceninja: |
name: "Invoiceninja Billing"
---
# This list of OAuth clients with their corresponding apps is loaded by the
# Stackspin dashboard on startup, and it will insert any missing entries into
# the Stackspin database (`oauthclient_apps` table).
apiVersion: v1
kind: ConfigMap
metadata:
name: stackspin-oauthclients-custom
namespace: flux-system
data:
vikunja: vikunja
gitea: gitea
apiVersion: v1
kind: ConfigMap
metadata:
namespace: stackspin-apps
name: stackspin-nextcloud-override
data:
values.yaml: |
nextcloud:
# https://artifacthub.io/packages/helm/nextcloud/nextcloud#configuration
# https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/config_sample_php_parameters.html#user-experience
#'defaultapp' => 'dashboard,files',
# https://github.com/moment/moment/tree/2.18.1/locale
#'default_language' => 'en_de', # https://www.transifex.com/explore/languages/
#'theme' => 'dark',
nextcloud:
configs:
defaults.config.php: |-
<?php
$CONFIG = array (
'allow_user_to_change_display_name' => false,
'default_phone_region' => 'DE',
'default_locale' => 'en-gb',
'lost_password_link' => 'https://dashboard.${domain}/web/recovery',
'enable_previews' => true,
'preview_libreoffice_path' => '/usr/bin/libreoffice',
'enabledPreviewProviders' =>
array (
0 => 'OC\\Preview\\TXT',
1 => 'OC\\Preview\\MarkDown',
2 => 'OC\\Preview\\OpenDocument',
3 => 'OC\\Preview\\PDF',
4 => 'OC\\Preview\\MSOffice2003',
5 => 'OC\\Preview\\MSOfficeDoc',
6 => 'OC\\Preview\\Image',
7 => 'OC\\Preview\\Photoshop',
8 => 'OC\\Preview\\TIFF',
9 => 'OC\\Preview\\SVG',
10 => 'OC\\Preview\\Font',
11 => 'OC\\Preview\\MP3',
12 => 'OC\\Preview\\Movie',
13 => 'OC\\Preview\\MKV',
14 => 'OC\\Preview\\MP4',
15 => 'OC\\Preview\\AVI',
)
);
#extraVolumes:
# - name: nextcloud-onlyoffice-config
# configMap:
# name: nextcloud-onlyoffice-config-and-scripts
#extraVolumeMounts:
# - name: nextcloud-onlyoffice-config
# mountPath: /var/local
lifecycle:
postStartCommand:
- '/bin/bash'
- '-c'
- 'apt update && apt install ffmpeg imagemagick ghostscript libreoffice libreoffice-l10n-de libreoffice-help-de'
#- 'echo hiho && /bin/bash /var/local/setup-apps.sh'
apps:
# Basics
- name: bruteforcesettings
enabled: false
- name: password_policy
enabled: false
- name: photos
enabled: false
- name: passwords
enabled: false
- name: contacts
enabled: true
# Common Apps
- name: external
enabled: true
- name: polls
enabled: false
#unsure - maybe redundant to forms?
- name: forms
enabled: true
#- name: spreed
# enabled: false
# redundant to Zulip & Jitsi
- name: appointments
enabled: true
# Management
- name: deck
enabled: true
- name: tasks
enabled: true
# Convenience
- name: files_rightclick
enabled: true
- name: metadata
enabled: true
- name: previewgenerator
enabled: true
# TODO handy but incompatible
#- name: extract
# enabled: false
# Collaborative apps
#- name: files_markdown
# enabled: false
#- name: files_texteditor
# enabled: true
- name: notes
enabled: true
- name: drawio
enabled: true
- name: files_mindmap
enabled: true
- name: maps
enabled: true
- name: jitsi
enabled: false
# waiting for /var/lib/Stackspin/local-storage/pvc-3b008674-544c-46e7-b456-f20932eb9f23_stackspin-apps_nextcloud-files/custom_apps
# Aesthetics
#- name: theming
# enabled: true
- name: apporder
enabled: false
- name: side_menu
enabled: true
- name: breezedark
enabled: true
#- name: unsplash
# enabled: true
# Flow
- name: analytics
enabled: true
- name: workflow_pdf_converter
enabled: true
#- name: files_accesscontrol
# enabled: false
#- name: files_automatedtagging
# enabled: false
#- name: workflow_media_converter
# enabled: false
#- name: workflow_ocr
# enabled: false
# Testing
- name: event_update_notification
enabled: false
#- name: calendar_resource_management
# enabled: true
# TODO install via CLI
# TODO disabling user status, files_comments, support, event_update_notification (Notifications for calendar event updates), bruteforcesettings
# need to disable tasks soon
# configure firstrunwizard
# preconfigured: share by mail - https://apps.nextcloud.com/apps/socialsharing_email
apiVersion: v1
kind: ConfigMap
metadata:
namespace: stackspin
name: stackspin-nginx-ingress-override
data:
values.yaml: |
# https://artifacthub.io/packages/helm/ingress-nginx/ingress-nginx
controller:
service:
externalTrafficPolicy: "Cluster"
annotations:
metallb.universe.tf/allow-shared-ip: "share-ipv4"
apiVersion: v1
kind: ConfigMap
metadata:
namespace: stackspin-apps
name: stackspin-zulip-override
data:
values.yaml: |
zulip:
environment:
SETTING_AUTHENTICATION_BACKENDS: '("zproject.backends.GenericOpenIdConnectBackend", "zproject.backends.EmailAuthBackend")'
#!/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()
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 }}"
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 }}"
---
apiVersion: v1
kind: Secret
metadata:
name: stackspin-{{ app }}-oauth-variables
data:
client_id: "{{ app | b64encode }}"
client_secret: "{{ 32 | generate_password | b64encode }}"
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 }}"
apiVersion: v1
kind: Secret
metadata:
name: stackspin-vikunja-variables
data:
jwt: "{{ 32 | generate_password | b64encode }}"
postgresql_password: "{{ 32 | generate_password | b64encode }}"