From b3b43329cd46be457b65709a4c92473a6660078d Mon Sep 17 00:00:00 2001 From: Alan Bridgeman Date: Tue, 11 Feb 2025 12:52:05 -0600 Subject: [PATCH] Initial Commit --- .gitignore | 5 + README.md | 42 ++++ TODO.md | 3 + create-helm-chart.ps1 | 2 + create-helm-chart.py | 99 ++++++++++ create-helm-chart.sh | 12 ++ input.example.json | 78 ++++++++ install.ps1 | 81 ++++++++ install.sh | 44 +++++ requirements.txt | 0 src/Cache.py | 52 +++++ src/Database.py | 142 ++++++++++++++ src/Deployment.py | 238 +++++++++++++++++++++++ src/HelmChart.py | 401 +++++++++++++++++++++++++++++++++++++++ src/Ingress.py | 39 ++++ src/MongoDB.py | 114 +++++++++++ src/NoSQL.py | 29 +++ src/OAuth.py | 40 ++++ src/Redis.py | 98 ++++++++++ src/Service.py | 23 +++ src/Template.py | 10 + src/ThirdPartyService.py | 30 +++ 22 files changed, 1582 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TODO.md create mode 100644 create-helm-chart.ps1 create mode 100644 create-helm-chart.py create mode 100644 create-helm-chart.sh create mode 100644 input.example.json create mode 100644 install.ps1 create mode 100644 install.sh create mode 100644 requirements.txt create mode 100644 src/Cache.py create mode 100644 src/Database.py create mode 100644 src/Deployment.py create mode 100644 src/HelmChart.py create mode 100644 src/Ingress.py create mode 100644 src/MongoDB.py create mode 100644 src/NoSQL.py create mode 100644 src/OAuth.py create mode 100644 src/Redis.py create mode 100644 src/Service.py create mode 100644 src/Template.py create mode 100644 src/ThirdPartyService.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93d0511 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Ignore the virtual environment +venv + +# Ignore any cache files +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b5dd96 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Automated Helm Chart Generator +This is a relatively simplistic Python app to simplify creating a Helm Chart more easily. + +Keep in mind, this is fairly opinionated and simplistic in it's implementation. This is on purpose. This largely developed for my own use rather than general applicability. + +## Getting Started +In general, there are 3 steps to run this automatation: +1. Run the appropriate installer script (technically this isn't required as you could just run the Python script directly but is for convenience) +2. Create and customize a `input.json` file that has the inputs for the script +3. Run the script + +### On Windows +Run the followng +```PowerShell +.\install.ps1 +``` + +Next copy the `input.example.json` into the folder you want (replacing `` with the appropriate path) and rename it `input.json`. You'll also probably want to customize this file (though not technically required) +```PowerShell +Copy-Item input.example.json \input.json +``` + +Then in the directory you want +```PowerShell +create-helm-chart +``` + +### On Mac/Linux (Uses Bash) +Run the followng +```sh +./install.sh +``` + +Next copy the `input.example.json` into the folder you want (replacing `` with the appropriate path) and rename it `input.json`. You'll also probably want to customize this file (though not technically required) +```sh +cp ./input.example.json /input.json +``` + +Then in the directory you want +```sh +create-helm-chart +``` \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..bba142f --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +# TO-DO List +- Make it so that there is more flexibility to not generate all the templates when run +- Create some kind of CLI/UI to create the `input.json` file or similar \ No newline at end of file diff --git a/create-helm-chart.ps1 b/create-helm-chart.ps1 new file mode 100644 index 0000000..d6a0d5a --- /dev/null +++ b/create-helm-chart.ps1 @@ -0,0 +1,2 @@ +# Call the Python script with the arguments passed to the PowerShell script +python $env:USERPROFILE\bin\automated-helm-generator\create-helm-chart.py $args \ No newline at end of file diff --git a/create-helm-chart.py b/create-helm-chart.py new file mode 100644 index 0000000..b6ad8d2 --- /dev/null +++ b/create-helm-chart.py @@ -0,0 +1,99 @@ +import json + +from src.Ingress import Ingress +from src.Service import Service +from src.Database import Database +from src.MongoDB import MongoDB +from src.Redis import Redis +from src.OAuth import OAuth +from src.ThirdPartyService import ThirdPartyService +from src.Deployment import Deployment +from src.HelmChart import HelmChart + +if __name__ == '__main__': + with open('input.json', 'r') as f: + data = json.load(f) + + # The API version of the Helm chart itself + api_version = data['chart']['apiVersion'] + # The version of the application that the Helm chart is deploying + app_version = data['chart']['appVersion'] + # A description of the Helm chart + chart_description = data['chart']['description'] + # The URL of the Helm chart's home page + chart_homepage = data['chart']['homepage'] + # The maintainers of the Helm chart + maintainers = data['chart']['maintainers'] + # The name of the Helm chart + chart_name = data['chart']['name'] + # The sources of the Helm chart + sources = data['chart']['sources'] + # The version of the Helm chart + chart_version = data['chart']['version'] + + image_repository = data['image']['repository'] + image_pull_policy = data['image']['pullPolicy'] + + hostname = data['ingress']['hostname'] + + db_name = data['db']['name'] + db_host = data['db']['host'] + db_user = data['db']['user'] + db_password = data['db']['password'] + + nosql_db_name = data['nosql']['dbName'] + nosql_user = data['nosql']['user'] + nosql_password = data['nosql']['password'] + #activityProperties: activity + #eventProperties: eventProps + #localeProperties: locales + #organizerProperties: organizerProps + #userProperties: userProps + #templates: templates + tables = data['nosql']['tables'] + + cache_password = data['cache']['password'] + + base_app_url = data['oauth']['baseAppUrl'] + app_abbreviation = data['oauth']['appAbbreviation'] + app_name = data['oauth']['appName'] + service_name = data['oauth']['serviceName'] + dev_port = data['oauth']['devPort'] + client_id = data['oauth']['clientId'] + client_secret = data['oauth']['clientSecret'] + + extra_env_vars = data['extraEnvVars'] + for key, value in extra_env_vars.items(): + if not isinstance(value, dict) and value.find("'") != -1: + extra_env_vars[key] = value.replace("'", '"') + + openai_api_key = data['thirdPartyServices']['openai']['apiKey'] + + helm_registry = data['registry'] + + ingress = Ingress(hostname) + service = Service() + db = Database(db_name, db_host, db_user, db_password) + mongo = MongoDB(nosql_db_name, nosql_user, nosql_password, tables) + redis = Redis(cache_password) + oauth = OAuth(base_app_url, app_abbreviation, app_name, service_name, dev_port, client_id, client_secret) + openai = ThirdPartyService('openai', True, api_key=openai_api_key) + deployment = Deployment(image_repository, image_pull_policy=image_pull_policy, uses_db=True, nosql=mongo, uses_cache=True, third_party_services=[openai], **extra_env_vars) + templates = [ingress, service, db, mongo, redis, oauth, deployment, openai] + helmChart = HelmChart(chart_name, chart_description, maintainers, chart_homepage, sources, app_version, chart_version, api_version, *templates) + helmChart.create_templates_folder() + helmChart.write_yaml() + helmChart.write_values_yaml() + helmChart.write_helmignore() + + try: + helmChart.package() + + try: + helmChart.push(helm_registry) + except Exception as ex: + print('Push to the registry failed. Please check the error message below:') + print(ex) + except Exception as e: + print('Packaging the Helm chart failed. Please check the error message below:') + print(e) diff --git a/create-helm-chart.sh b/create-helm-chart.sh new file mode 100644 index 0000000..14b7922 --- /dev/null +++ b/create-helm-chart.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Check if the call should use `python` or `python3` +if command -v python3 &> /dev/null +then + python=python3 +else + python=python +fi + +# Call the Python script with the arguments passed to the PowerShell script +$python $(realpath ./create-helm-chart.py) $args \ No newline at end of file diff --git a/input.example.json b/input.example.json new file mode 100644 index 0000000..f8fac47 --- /dev/null +++ b/input.example.json @@ -0,0 +1,78 @@ +{ + "chart": { + "apiVersion": "v1", + "appVersion": "1.0.0", + "description": "A Helm chart for deploying .", + "homepage": "", + "maintainers": [ + { + "name": "", + "email": "" + } + ], + "name": "", + "sources": [ + "" + ], + "version": "1.0.0" + }, + "image": { + "repository": "/", + "pullPolicy": "IfNotPresent" + }, + "ingress": { + "hostname": "" + }, + "db": { + "name": "", + "host": "", + "user": "", + "password": "" + }, + "nosql": { + "dbName": "", + "user": "", + "password": "", + "tables": { + "": { + "name": "
", + "value": "" + } + } + }, + "cache": { + "password": "" + }, + "oauth": { + "baseAppUrl": "", + "appAbbreviation": "", + "appName": "", + "serviceName": "", + "devPort": "", + "clientId": "", + "clientSecret": "" + }, + "thirdPartyServices": { + "openai": { + "apiKey": "" + } + }, + "extraEnvVars": { + "": { + "type": "Secret", + "name": "{{ .Release.Name }}-", + "key": "", + "description": "", + "value": "" + }, + "": { + "type": "ConfigMap", + "name": "{{ .Release.Name }}-", + "key": "", + "description": "", + "value": "" + }, + "": "''" + }, + "registry": "" +} \ No newline at end of file diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..b5336a1 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,81 @@ +# ************************************************************************************* +# | Install the scripts for use by the user without having to specify the full | +# | path to them. | +# | | +# | This script will: | +# | 1. Check if the bin directory already exists in the user's home directory | +# | 2. If it doesn't exist, create it | +# | 3. Add the bin directory to the user's PATH (permanently) | +# | 4. Install the scripts to the bin directory | +# | | +# | Usage: | +# | 1. Open a PowerShell terminal | +# | 2. Run the script `.\install.ps1` | +# | 3. Use the `create-helm-chart` command to create a chart in the current directory | +# | | +# | --------------------------------------------------------------------------------- | +# | Author: [Bridgeman Accessible](https://bridgemanaccessible.ca) | +# | Date: 2025-02-11 | +# | Version: 1.0.0 | +# | --------------------------------------------------------------------------------- | +# | MIT License | +# | | +# | Copyright (c) 2025 Bridgeman Accessible / Alan Bridgeman | +# | | +# | Permission is hereby granted, free of charge, to any person obtaining a copy | +# | of this software and associated documentation files (the "Software"), to deal | +# | in the Software without restriction, including without limitation the rights | +# | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | +# | copies of the Software, and to permit persons to whom the Software is | +# | furnished to do so, subject to the following conditions: | +# | | +# | The above copyright notice and this permission notice shall be included in all | +# | copies or substantial portions of the Software. | +# | | +# | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | +# | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | +# | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | +# | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | +# | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | +# | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | +# | SOFTWARE. | +# ************************************************************************************* + +Write-Host "Installing automated-helm-generator to $($env:USERPROFILE)\bin ..." + +# Check if the bin directory already exists in the user's home directory +# If it doesn't exist, create it +# This is so that we can have a predictable location to install scripts to +if (-not (Test-Path $env:USERPROFILE\bin)) { + Write-Host "Creating %USERPROFILE%\bin" + mkdir $env:USERPROFILE\bin +} +else { + Write-Host "$($env:USERPROFILE)\bin already exists" +} + +# Because we want the scripts to be usable without having to specify the full path to them we need to add the scripts folder to the user's PATH +# Check if the bin directory is already on the user's PATH +if (-not [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User).Contains("%USERPROFILE%\bin")) { + Write-Host "Adding $($env:USERPROFILE)\bin (%USERPROFILE%\bin) to the user's PATH" + + # Add the bin directory to the user's PATH permanently + [Environment]::SetEnvironmentVariable( + "Path", + [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User) + ";%USERPROFILE%\bin", + [EnvironmentVariableTarget]::User) + + Write-Host "You will likely need to restart PowerShell for the changes to take effect" +} +else { + Write-Host "$($env:USERPROFILE)\bin (%USERPROFILE%\bin) is already on the user's PATH" +} + +mkdir $env:USERPROFILE\bin\automated-helm-generator + +# Install the script to the bin directory +Copy-Item -Recurse .\src $env:USERPROFILE\bin\automated-helm-generator\src +Copy-Item .\create-helm-chart.py $env:USERPROFILE\bin\automated-helm-generator\create-helm-chart.py +Copy-Item .\create-helm-chart.ps1 $env:USERPROFILE\bin\create-helm-chart.ps1 + +Write-Host "Installed successfully to $($env:USERPROFILE)\bin" \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e59f84f --- /dev/null +++ b/install.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# ************************************************************************************* +# | Install the scripts for use by the user without having to specify the full | +# | path to them. | +# | | +# | This script will: | +# | 1. Creates symlinks for the scripts in the `/usr/local/bin`` directory | +# | | +# | Usage: | +# | 1. Open a Bash terminal | +# | 2. Run the script `.\install-scripts.sh` | +# | 3. Use the `create-helm-chart` command to create a chart in the current directory | +# | | +# | --------------------------------------------------------------------------------- | +# | Author: [Bridgeman Accessible](https://bridgemanaccessible.ca) | +# | Date: 2025-02-11 | +# | Version: 1.0.0 | +# | --------------------------------------------------------------------------------- | +# | MIT License | +# | | +# | Copyright (c) 2025 Bridgeman Accessible / Alan Bridgeman | +# | | +# | Permission is hereby granted, free of charge, to any person obtaining a copy | +# | of this software and associated documentation files (the "Software"), to deal | +# | in the Software without restriction, including without limitation the rights | +# | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | +# | copies of the Software, and to permit persons to whom the Software is | +# | furnished to do so, subject to the following conditions: | +# | | +# | The above copyright notice and this permission notice shall be included in all | +# | copies or substantial portions of the Software. | +# | | +# | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | +# | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | +# | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | +# | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | +# | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | +# | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | +# | SOFTWARE. | +# ************************************************************************************* + +# Install the script by creating a symlink in the `/usr/local/bin` directory +ln -s $(realpath ./create-helm-chart.sh) /usr/local/bin/create-helm-chart \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Cache.py b/src/Cache.py new file mode 100644 index 0000000..dd0c3fc --- /dev/null +++ b/src/Cache.py @@ -0,0 +1,52 @@ +from .Template import Template + +class Cache (Template): + def __init__(self, password: str, hostName: str, port: str, create: bool): + """A class for creating a/some template(s) related to a cache server. + + Args: + password (str): The password to access the cache server. + hostName (str): The hostname of the cache server. + port (str): The port of the cache server. + create (bool): Whether or not to create the cache server as part of the Helm Chart deployment. + """ + + super().__init__() + + self.password = password + self.hostName = hostName + self.port = port + self.create = create + + def write_generic_cache_templates(self, type: str, default_hostname: str): + """Write the generic cache templates to a file. + + Args: + type (str): The type of cache server. Relevant if the cache server is to be created as the default hostname will be used if the type matches with whats provided in the `values.yaml` file. + default_hostname (str): The default hostname of the cache server. Used if the cache server is to be created and the type matches with whats provided in the `values.yaml` file. + """ + + # Create the configmap file that holds the hostname and port of the cache server + with open('templates/cache-configmap.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: ConfigMap' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-cache-configmap' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('data:' + '\n') + f.write(' ' + '{{- if and (eq .Values.cache.type "' + type + '") (.Values.cache.create) }}' + '\n') + f.write(' ' + f'hostname: {default_hostname}' + '\n') + f.write(' ' + '{{- else }}' + '\n') + f.write(' ' + 'hostname: {{ .Values.cache.hostname }}' + '\n') + f.write(' ' + '{{- end }}' + '\n') + f.write(' ' + 'port: {{ .Values.cache.port }}' + '\n') + + # Create the credentials secret file + with open('templates/cache-credentials-secret.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-cache-credentials' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'password: {{ .Values.cache.password | b64enc }}' + '\n') \ No newline at end of file diff --git a/src/Database.py b/src/Database.py new file mode 100644 index 0000000..cbc7122 --- /dev/null +++ b/src/Database.py @@ -0,0 +1,142 @@ +from .Template import Template + +class Database (Template): + def __init__(self, name: str, host: str, user: str, password: str, create: bool = True, port: int = 5432, instance_id: str = ''): + """A class for creating a/some template(s) related to a database. + + Args: + name (str): The name of the database to be used. + host (str): The host that the database is located on. + user (str): The user that is used to access the database. + password (str): The password that is used to access the database. + create (bool, Optional): If set to `True`, the database will be created as part of the deployment. This uses the [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database. Default True + port (int, Optional): The port that the database listens on. Default 5432 + instance_id (str, Optional): Allows for distinguishing between multiple database instances/servers. Default '' + """ + + # The type of the relational database that is used. + # + # The following table lists the possible values for this field: + # + # | Value | Description | + # | ---------- | ------------------------------------------ | + # | `postgres` | Uses PostgreSQL as the relational database | + # + # Note, for use of `postgres`, it uses a [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database + # + self.type = 'postgres' + + # If set to `true`, the database will be created as part of the deployment + # This uses the [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database + self.create = create + + # The host that the database is located on + self.host = host + + # The name of the database to be used + self.name = name + + # The user that is used to access the database + self.user = user + + # The password that is used to access the database + self.password = password + + # The port that the database listens on + self.port = port + + # Allows for distinguishing between multiple database instances/servers + self.instance_id = instance_id + + def write(self): + # Config Map file for use within the Postgres Controller namespace + # This is required by the operator to function properly + with open('templates/db-credentials-config-map-postgres-controller.yaml', 'w') as f: + f.write('{{- if and (eq .Values.database.type "postgres") (.Values.database.create) -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: ConfigMap' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + 'namespace: postgres-controller' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'db-host: {{ .Values.database.host }}' + '\n') + f.write(' ' + 'db-name: {{ .Values.database.name }}' + '\n') + f.write(' ' + 'db-user: {{ .Values.database.user }}' + '\n') + f.write(' ' + '{{- if .Values.database.port }}' + '\n') + f.write(' ' + 'db-port: {{ .Values.database.port | quote }}' + '\n') + f.write(' ' + '{{- else }}' + '\n') + f.write(' ' + 'db-port: "5432"' + '\n') + f.write(' ' + '{{- end }}' + '\n') + f.write('{{- end -}}' + '\n') + + # Config Map file in the same namespace as the app + with open('templates/db-credentials-config-map.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: ConfigMap' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-db-credentials' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'db-host: {{ .Values.database.host }}' + '\n') + f.write(' ' + 'db-name: {{ .Values.database.name }}' + '\n') + f.write(' ' + 'db-user: {{ .Values.database.user }}' + '\n') + f.write(' ' + '{{- if .Values.database.port }}' + '\n') + f.write(' ' + 'db-port: {{ .Values.database.port | quote }}' + '\n') + f.write(' ' + '{{- else }}' + '\n') + f.write(' ' + 'db-port: "5432"' + '\n') + f.write(' ' + '{{- end }}' + '\n') + + # Secret file for the password to access the database for use within the Postgres Controller namespace + # This is required by the operator to function properly + with open('templates/db-password-secret-postgres-controller.yaml', 'w') as f: + f.write('{{- if and (eq .Values.database.type "postgres") (.Values.database.create) -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-db-password' + '\n') + f.write(' ' + 'namespace: postgres-controller' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'password: {{ .Values.database.password | b64enc }}' + '\n') + f.write('{{- end -}}' + '\n') + + # Secret file for the password to access the database in the same namespace as the app + with open('templates/db-password-secret.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-db-password' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'password: {{ .Values.database.password | b64enc }}' + '\n') + + # Custom Resource Definition (CRD) file to create the database using the operator + with open('templates/database.yaml', 'w') as f: + f.write('{{- if and (eq .Values.database.type "postgres") (.Values.database.create) -}}' + '\n') + f.write('apiVersion: postgresql.org/v1' + '\n') + f.write('kind: PostgresDatabase' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-db' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'dbName:' + '\n') + f.write(' ' + ' ' + 'envFrom:' + '\n') + f.write(' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'namespace: postgres-controller' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'key: db-name' + '\n') + f.write(' ' + 'dbRoleName:' + '\n') + f.write(' ' + ' ' + 'envFrom:' + '\n') + f.write(' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'namespace: postgres-controller' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'key: db-user' + '\n') + f.write(' ' + 'dbRolePassword:' + '\n') + f.write(' ' + ' ' + 'envFrom:' + '\n') + f.write(' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: {{ .Release.Name }}-db-password' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'namespace: postgres-controller' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'key: password' + '\n') + f.write('{{- if .Values.database.instance_id }}' + '\n') + f.write(' ' + 'dbInstanceId: {{ .Values.database.instance_id }}' + '\n') + f.write('{{- end }}' + '\n') + f.write('{{- end -}}' + '\n') \ No newline at end of file diff --git a/src/Deployment.py b/src/Deployment.py new file mode 100644 index 0000000..c0e97e5 --- /dev/null +++ b/src/Deployment.py @@ -0,0 +1,238 @@ +from .Template import Template +from .NoSQL import NoSQL +from .ThirdPartyService import ThirdPartyService + +class Deployment (Template): + def __init__(self, image_repository: str, image_tag: str = 'v1.0.0', image_pull_policy: str = 'IfNotPresent', replica_count: int = 1, port: int = 8080, env: str = 'production', uses_oauth: bool = True, uses_db: bool = False, nosql: NoSQL | None = None, uses_cache: bool = False, third_party_services: list[ThirdPartyService] = [], **extra_env_vars: dict[str, str | dict[str, str]]): + """A class for creating a/some template(s) related to the Deployment for the app. + + Args: + image_repository (str): The repository of the image to be used for the Deployment. + image_tag (str, Optional): The tag of the image to be used for the Deployment. Default 'v1.0.0' + image_pull_policy (str, Optional): The image pull policy to be used for the Deployment. Default 'IfNotPresent' + replica_count (int, Optional): The number of replicas of the app to be running. Default 1 + port (int, Optional): The port the app will be running on. Default 8080 + env (str, Optional): The environment the app will be running in. Default 'production' + uses_oauth (bool, Optional): Whether or not OAuth is to be used. Determines if OAuth related environment variables need to be set on the Deployment. Default True + uses_db (bool, Optional): Whether or not a database is to be used. Determines if database related environment variables need to be set on the Deployment. Default False + nosql (NoSQL, Optional): The NoSQL template. If set, Determines if NoSQL database related environment variables need to be set on the Deployment. We require the object to get table names to set appropriate environment variables on the Deployment. Default None + uses_cache (bool, Optional): Whether or not a cache server is to be used. Determines if cache related environment variables need to be set on the Deployment. Default False + third_party_services (list[ThirdPartyService], Optional): The third party services to be used. Determines if third party service related environment variables need to be set on the Deployment. Default empty list (`[]`) + extra_env_vars (dict[str, str | dict[str, str]]): Extra environment variables to be set on the Deployment. The key is the name of the environment variable and the value, if it's a string, is the value of the environment variable. If the value is a dictionary, than a file for the value as a secret or configmap is created and referenced by the environment variable. + """ + + super().__init__() + + self.image_repository = image_repository + self.image_tag = image_tag + self.image_pull_policy = image_pull_policy + self.replica_count = replica_count + self.env = env + self.port = port + self.uses_oauth = uses_oauth + self.uses_db = uses_db + self.nosql = nosql + self.uses_cache = uses_cache + self.third_party_services = third_party_services + self.extra_env_vars = extra_env_vars + + def write(self): + """Write the Deployment template to a file.""" + + for value in self.extra_env_vars.values(): + if isinstance(value, dict): + if value['type'] == 'Secret': + filename = value['name'] + if filename.startswith('{{ .Release.Name }}'): + filename = filename.replace('{{ .Release.Name }}-', '') + + snake_case_name = filename.split('-')[0] + for token in filename.split('-'): + if token != snake_case_name: + snake_case_name += token.capitalize() + + with open(f'templates/{filename}-secret.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + f'name: {value["name"]}' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + f'{value["key"]}: ' + '{{ .Values.' + snake_case_name + ' | b64enc }}' + '\n') + + with open(f'templates/deployment.yaml', 'w') as f: + f.write('apiVersion: apps/v1' + '\n') + f.write('kind: Deployment' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}' + '\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'replicas: {{ .Values.replicaCount }}' + '\n') + f.write(' ' + 'selector:' + '\n') + f.write(' ' + ' ' + 'matchLabels:' + '\n') + f.write(' ' + ' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write(' ' + 'template:' + '\n') + f.write(' ' + ' ' + 'metadata:' + '\n') + f.write(' ' + ' ' + ' ' + 'labels:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write(' ' + ' ' + 'spec:' + '\n') + f.write(' ' + ' ' + ' ' + 'containers:' + '\n') + f.write(' ' + ' ' + ' ' + '- name: {{ .Release.Name }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'image: {{ .Values.image.repository }}:{{ .Values.image.tag }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'imagePullPolicy: {{ .Values.image.pullPolicy }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'ports:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- containerPort: {{ .Values.container.port }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'env:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: NODE_ENV' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'value: {{ .Values.container.env }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: PORT' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'value: "{{ .Values.container.port }}"' + '\n') + + for key, value in self.extra_env_vars.items(): + # Check if the value is a dictionary or a string + if isinstance(value, dict): + f.write(' ' + ' ' + ' ' + ' ' + f'- name: {key.upper()}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + if value['type'] == 'Secret': + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + elif value['type'] == 'ConfigMap': + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: ' + value['name'] + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: ' + value['key'] + '\n') + else: + f.write(' ' + ' ' + ' ' + ' ' + f'- name: {key.upper()}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + f'value: {value}' + '\n') + + if self.uses_oauth: + f.write(' ' + ' ' + ' ' + ' ' + '# OAuth Implementation Stuff' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: BASE_APP_URL' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: base-app-url' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: APP_ABBRV' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: app-abbreviation' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: APP_NAME' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: app-name' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: SERVICE_NAME' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: service-name' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: DEV_PORT' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: dev-port' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: CLIENT_ID' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: client-id' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: CLIENT_SECRET' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-oauth-client-secret' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: client-secret' + '\n') + + if self.uses_db: + f.write(' ' + ' ' + ' ' + ' ' + '# Database credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: DB_HOST' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: db-host' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: DB_NAME' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: db-name' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: DB_PASSWORD' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-db-password' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: password' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: DB_PORT' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: db-port' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: DB_USER' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-db-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: db-user' + '\n') + + if self.nosql is not None: + f.write(' ' + ' ' + ' ' + ' ' + '# NoSQL Credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '{{- if eq .Values.nosql.type "mongodb" }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: STORAGE_ACCOUNT_CONNECTION_STRING' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-mongo-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: connection-string' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '{{- else if eq .Values.nosql.type "azure" }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: STORAGE_ACCOUNT_KEY' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-azure-tables-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: key' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: STORAGE_ACCOUNT_NAME' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-azure-tables-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: name' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '{{- end }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '# NoSQL Table Names' + '\n') + + for key, value in self.nosql.tables.items(): + f.write(' ' + ' ' + ' ' + ' ' + f'- name: {key.upper()}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-storage-tables' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + f'key: {value["name"]}' + '\n') + + if self.uses_cache: + f.write(' ' + ' ' + ' ' + ' ' + '# Caching Server Variables' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: CACHE_HOSTNAME' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Relese.name }}-cache-configmap' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: hostname' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: CACHE_PORT' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-cache-configmap' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: port' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: CACHE_PASSWORD' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-cache-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: password' + '\n') + + f.write(' ' + ' ' + ' ' + ' ' + '# Third-Party Integrations' + '\n') + for third_party in self.third_party_services: + f.write(' ' + ' ' + ' ' + ' ' + '{{- if .Values.thirdParty.' + third_party.name + '.enabled }}' + '\n') + + for var in third_party.vars: + f.write(' ' + ' ' + ' ' + ' ' + '- name: ' + third_party.name.upper() + '_' + var.upper() + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-' + third_party.name + '-secret' + '\n') + f.write(' ' + ' ' ' ' + ' ' + ' ' + ' ' + ' ' + f'key: {var.replace("_", "-")}' + '\n') + + f.write(' ' + ' ' + ' ' + ' ' + '{{- end }}' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + '{{- if .Values.thirdParty.openai.enabled }}' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + '- name: OPEANAI_API_KEY' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-openai-secret' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: api-key' + '\n') + #f.write(' ' + ' ' + ' ' + ' ' + '{{- end }}' + '\n') \ No newline at end of file diff --git a/src/HelmChart.py b/src/HelmChart.py new file mode 100644 index 0000000..0b334d8 --- /dev/null +++ b/src/HelmChart.py @@ -0,0 +1,401 @@ +import os, subprocess + +from .Template import Template +from .Ingress import Ingress +from .Database import Database +from .NoSQL import NoSQL +from .MongoDB import MongoDB +from .Redis import Redis +from .OAuth import OAuth +from .ThirdPartyService import ThirdPartyService +from .Deployment import Deployment + +class HelmChart: + def __init__(self, chartName: str, chartDescription: str, maintainers: list[dict[str, str]], chartHomepage: str, sources: list[str], appVersion: str = '1.0.0', chartVersion: str = '1.0.0', apiVersion: str = 'v1', *templates: Template): + """A class for creating a Helm chart. + + Args: + chartName (str): The name of the Helm chart. + chartDescription (str): A description of the Helm chart. + maintainers (list[dict[str, str]]): The maintainers of the Helm chart. + chartHomepage (str): The URL of the Helm chart's home page + sources (list[str]): The sources of the Helm chart. + appVersion (str, Optional): The version of the application that the Helm chart is deploying. Default '1.0.0' + chartVersion (str, Optional): The version of the Helm chart. Default '1.0.0' + apiVersion (str, Optional): The API version of the Helm chart itself. Default 'v1' + Templates (Template, Optional): The templates for the Helm chart. Default None + """ + + self.chartName = chartName + self.chartDescription = chartDescription + self.maintainers = maintainers + self.chartHomepage = chartHomepage + self.sources = sources + self.appVersion = appVersion + self.chartVersion = chartVersion + self.apiVersion = apiVersion + self.templates = templates + + def create_templates_folder(self): + """Create the templates folder for the Helm chart.""" + + os.mkdir('templates') + + for template in self.templates: + template.write() + + def write_yaml(self): + """Write the Chart.yaml file for the Helm chart.""" + + with open('Chart.yaml', 'w') as f: + f.write(f'apiVersion: {self.apiVersion}' + '\n') + f.write(f'appVersion: "{self.appVersion}"' + '\n') + f.write(f'description: {self.chartDescription}' + '\n') + f.write(f'home: {self.chartHomepage}' + '\n') + f.write('maintainers:' + '\n') + for maintainer in self.maintainers: + f.write(' ' + f'- email: {maintainer["email"]}' + '\n') + f.write(' ' + ' ' + f'name: {maintainer["name"]}' + '\n') + f.write(f'name: {self.chartName}' + '\n') + f.write('sources:' + '\n') + for source in self.sources: + f.write(f'- {source}' + '\n') + f.write(f'version: "{self.chartVersion}"' + '\n') + + def write_values_yaml(self): + """Write the values.yaml file for the Helm chart.""" + + # Get the Deployment and Ingress templates from the templates provided + deployment_template = next(template for template in self.templates if isinstance(template, Deployment)) + ingress_template = next(template for template in self.templates if isinstance(template, Ingress)) + + with open('values.yaml', 'w') as f: + f.write('# The number of instances (replicas) of the app to run' + '\n') + f.write(f'replicaCount: {deployment_template.replica_count}' + '\n') + f.write('\n') + + f.write('image:' + '\n') + f.write(' ' + '# The repository of the image to use for the app' + '\n') + f.write(' ' + '# Should be in the format `/`' + '\n') + f.write(' ' + f'repository: "{deployment_template.image_repository}"' + '\n') + f.write(' ' + '# The specific image tag to use. It\'s recommended to use some kind of versioning tag scheme as it makes updating the container without having to fully redeploy easier.' + '\n') + f.write(' ' + '# Ex. v1.0.0' + '\n') + f.write(' ' + f'tag: "{deployment_template.image_tag}"' + '\n') + f.write(' ' + '# How often the image should be pulled. The possible values are "Always", "Never", and "IfNotPresent"' + '\n') + f.write(' ' + '# It\'s recommended for production to use "IfNotPresent" to avoid pulling the image every time the pod starts' + '\n') + f.write(' ' + '# Though, for development, "Always" is recommended to ensure the latest changes are being tested' + '\n') + f.write(' ' + f'pullPolicy: "{deployment_template.image_pull_policy}"' + '\n') + f.write('\n') + + f.write('container:' + '\n') + f.write(' ' + '# The port that the container listens on (Ex. 8080)' + '\n') + f.write(' ' + f'port: {deployment_template.port}' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# The environment that the container is running in (Ex. development, production, etc...)' + '\n') + f.write(' ' + '# This is used for the NODE_ENV environment variable' + '\n') + f.write(' ' + f'env: "{deployment_template.env}"' + '\n') + f.write('\n') + + f.write('ingress:' + '\n') + f.write(' ' + '# We want an ingress resource if we are deploying to a cluster that has a ingress controller/load balancer' + '\n') + f.write(' ' + '# This includes most public cloud providers like EKS, GKE, and AKS' + '\n') + f.write(' ' + 'enabled: true' + '\n') + f.write(' ' + '# The DNS Name (Ex. app.example.com) where the app will be accessible' + '\n') + f.write(' ' + f'host: "{ingress_template.hostname}"' + '\n') + f.write(' ' + '# The class of the ingress controller that is being used (defaulted here to an NGINX ingress controller as it\'s popular for Kubernetes clusters)' + '\n') + f.write(' ' + 'class: nginx' + '\n') + f.write('\n') + + for value in deployment_template.extra_env_vars.values(): + if isinstance(value, dict): + # If a description is provided for the environment variable than we want to include it as a comment above the value in the `values.yaml` file. + if 'description' in value: + f.write(f'# {value["description"]}' + '\n') + + var_name = value["name"].replace('{{ .Release.Name }}-', '') + snake_case_name = var_name.split('-')[0] + for token in var_name.split('-'): + if token != snake_case_name: + snake_case_name += token.capitalize() + + f.write(f'{snake_case_name}: "{value["value"]}"' + '\n') + #f.write('privateRegistryToken: ""' + '\n') + f.write('\n') + + # If a OAuth template is included in the provided templates than we want to include the appropriate section to the `values.yaml` file. + if any(isinstance(template, OAuth) for template in self.templates): + # Get the OAuth template from the templates provided + oauth_template = next(template for template in self.templates if isinstance(template, OAuth)) + + f.write('# Configuration for using OAuth within the app' + '\n') + f.write('oauth:' + '\n') + f.write(' ' + f'baseAppUrl: "{oauth_template.base_app_url}"' + '\n') + f.write(' ' + f'appAbbreviation: "{oauth_template.app_abbreviation}"' + '\n') + f.write(' ' + f'appName: "{oauth_template.app_name}"' + '\n') + f.write(' ' + f'serviceName: "{oauth_template.service_name}"' + '\n') + f.write(' ' + f'devPort: "{oauth_template.dev_port}"' + '\n') + f.write(' ' + f'clientId: "{oauth_template.client_id}"' + '\n') + f.write(' ' + f'clientSecret: "{oauth_template.client_secret}"' + '\n') + f.write('\n') + + # If a Database template is included in the provided templates than we want to include the appropriate section to the `values.yaml` file. + if any(isinstance(template, Database) for template in self.templates): + # Get the Database template from the templates provided + database_template = next(template for template in self.templates if isinstance(template, Database)) + + f.write('# Configuration for the relational database' + '\n') + f.write('database:' + '\n') + f.write(' ' + '# The type of the relational database that is used.' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + '# The following table lists the possible values for this field:' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + '# | Value | Description |' + '\n') + f.write(' ' + '# | ---------- | ------------------------------------------ |' + '\n') + f.write(' ' + '# | `postgres` | Uses PostgreSQL as the relational database |' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + '# Note, for use of `postgres`, it uses a [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + f'type: "{database_template.type}"' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# If set to `true`, the database will be created as part of the deployment' + '\n') + f.write(' ' + '# This uses the [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database' + '\n') + f.write(' ' + f'create: {str(database_template.create).lower()}' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# The host that the database is located on ' + '\n') + f.write(' ' + f'host: "{database_template.host}"' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# The name of the database to be used' + '\n') + f.write(' ' + f'name: "{database_template.name}"' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# The user that is used to access the database' + '\n') + f.write(' ' + f'user: "{database_template.user}"' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# The password that is used to access the database' + '\n') + f.write(' ' + f'password: "{database_template.password}"' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# The port that the database listens on' + '\n') + f.write(' ' + f'#port: {database_template.port}' + '\n') + f.write(' ' + '\n') + f.write(' ' + '# Allows for distinguishing between multiple database instances/servers' + '\n') + f.write(' ' + f'#instance_id: "{database_template.instance_id}"' + '\n') + f.write('\n') + + # If a Database template is included in the provided templates than we want to include the appropriate section to the `values.yaml` file. + if any(isinstance(template, NoSQL) for template in self.templates): + nosql_template = next(template for template in self.templates if isinstance(template, NoSQL)) + + f.write('# Configuration the NoSQL database' + '\n') + f.write('# Within the parlance of the system these are often called "properties" databases (and store less structured data)' + '\n') + f.write('nosql:' + '\n') + f.write(' ' + '# Determines the type of NoSQL storage that is used' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + '# The following table lists the possible values for this field:' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + '# | Value | Description |' + '\n') + f.write(' ' + '# | --------- | ------------------------------------------------------------------------------------------ |' + '\n') + f.write(' ' + '# | `mongodb` | Uses MongoDB as the NoSQL database for the default account properties database |' + '\n') + f.write(' ' + '# | `azure` | Uses Azure Table Storage as the NoSQL database for the default account properties database |' + '\n') + f.write(' ' + '# ' + '\n') + f.write(' ' + f'type: {nosql_template.type}' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# If to create a resource as part of the deployment process' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `mongodb`' + '\n') + f.write(' ' + '# This uses the [MongoDBCommunity CRD](https://github.com/mongodb/mongodb-kubernetes-operator) to create the resource' + '\n') + f.write(' ' + f'create: {str(nosql_template.create).lower()}' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The number of replicas/members as part of the Mongo deployment' + '\n') + f.write(' ' + '# See the `member` parameter of the [MongoDBCommunity CRD](https://github.com/mongodb/mongodb-kubernetes-operator) for more information' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `mongodb` and `create` is set to `true`' + '\n') + if isinstance(nosql_template, MongoDB): + f.write(' ' + f'replicaCount: {nosql_template.replica_count}' + '\n') + else: + f.write(' ' + '#replicaCount: ' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The TLS configuration for the connection to the NoSQL database' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `mongodb` and `create` is set to `true`' + '\n') + f.write(' ' + 'tls:' + '\n') + f.write(' ' + ' ' + '# If to use TLS for the connection to the NoSQL database' + '\n') + if isinstance(nosql_template, MongoDB): + f.write(' ' + ' ' + f'enabled: {str(nosql_template.tls_enabled).lower()}' + '\n') + else: + f.write(' ' + ' ' + 'enabled: ' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The connection string used to access the NoSQL database' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `mongodb` and `create` is set to `false`' + '\n') + f.write(' ' + '# Should be in the following format: `mongodb://:`' + '\n') + f.write(' ' + '#connectionString: "mongodb://mongo.example.com:27017"' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The key used to access the NoSQL database' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `azure`' + '\n') + f.write(' ' + '#key: ""' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The name of the NoSQL database' + '\n') + f.write(' ' + f'name: "{nosql_template.db_name}"' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The username used to access the NoSQL database' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `mongodb`' + '\n') + if isinstance(nosql_template, MongoDB): + f.write(' ' + f'user: "{nosql_template.user}"' + '\n') + else: + f.write(' ' + 'user: ""' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The password used to access the NoSQL database' + '\n') + f.write(' ' + '# ONLY relevant if `type` is set to `mongodb`' + '\n') + if isinstance(nosql_template, MongoDB): + f.write(' ' + f'password: "{nosql_template.password}"' + '\n') + else: + f.write(' ' + 'password: ""' + '\n') + f.write('\n') + + f.write('# Configurable NoSQL information groupings' + '\n') + f.write('# For Azure Table STorage these are table names' + '\n') + f.write('# For MongoDB these are collection names' + '\n') + f.write('tables:' + '\n') + + for value in nosql_template.tables.values(): + snake_case_name = value['name'].split('-')[0] + for token in value['name'].split('-'): + if token != snake_case_name: + snake_case_name += token.capitalize() + f.write(' ' + f'{snake_case_name}: "{value["value"]}"' + '\n') + #f.write(' ' + 'activityProperties: activity' + '\n') + #f.write(' ' + 'eventProperties: eventProps' + '\n') + #f.write(' ' + 'localeProperties: locales' + '\n') + #f.write(' ' + 'organizerProperties: organizerProps' + '\n') + #f.write(' ' + 'userProperties: userProps' + '\n') + #f.write(' ' + 'templates: templates' + '\n') + f.write('\n') + + # If a Redis template is included in the provided templates than we want to include the appropriate section to the values.yaml file. + if any(isinstance(template, Redis) for template in self.templates): + # Get the Redis template from the templates provided + redis_template = next(template for template in self.templates if isinstance(template, Redis)) + + f.write('# Configuration for cache server' + '\n') + f.write('cache:' + '\n') + f.write(' ' + 'type: "redis"' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# If to create a Redis instance/resource as part of the deployment process' + '\n') + f.write(' ' + f'create: {str(redis_template.create).lower()}' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The image to use for the Redis instance' + '\n') + f.write(' ' + '# ONLY relevant if `create` is set to `true`' + '\n') + # If the image dictionary is not empty than we want to include the image values + if redis_template.create and len(redis_template.image) > 0: + f.write(' ' + 'image' + '\n') + + # Loop through the image dictionary and write the image values + for key, value in redis_template.image.items(): + f.write(' ' + ' ' + f'{key}: "{value}"' + '\n') + else: + f.write(' ' + 'image: {}' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The number of replicas of the Redis instance' + '\n') + f.write(' ' + '# ONLY relevant if `create` is set to `true`' + '\n') + if redis_template.create: + f.write(' ' + f'replicaCount: {redis_template.replicaCount}' + '\n') + else: + f.write(' ' + '#replicaCount: ' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# Hostname of the Redis server' + '\n') + f.write(' ' + '# ONLY relevant if `create` is set to `false`' + '\n') + if redis_template.create: + f.write(' ' + '#hostName: ""' + '\n') + else: + f.write(' ' + f'hostName: "{redis_template.hostName}"' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The password to use for the Redis server' + '\n') + f.write(' ' + f'password: "{redis_template.password}"' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# The port of the Redis server' + '\n') + f.write(' ' + f'port: "{redis_template.port}"' + '\n') + f.write(' ' + '\n') + + f.write(' ' + '# Redis TLS Configurations' + '\n') + f.write(' ' + 'tls:' + '\n') + f.write(' ' + ' ' + '# If TLS is enabled for the Redis instance' + '\n') + f.write(' ' + ' ' + f'enabled: {str(redis_template.tls_enabled).lower()}' + '\n') + f.write(' ' + ' ' + '\n') + f.write(' ' + ' ' + '# The port of the Redis instance for TLS' + '\n') + f.write(' ' + ' ' + '# ONLY relevant if `tls.enabled` is set to `true`' + '\n') + if redis_template.tls_enabled: + f.write(' ' + ' ' + f'port: "{redis_template.tls_port}"' + '\n') + else: + f.write(' ' + ' ' + '#port: ""' + '\n') + f.write(' ' + '\n') + + f.write('# Configurations for integration with third-party services' + '\n') + f.write('thirdParty:' + '\n') + for template in self.templates: + if isinstance(template, ThirdPartyService): + f.write(' ' + '# Configurations for the ' + template.name.capitalize() + ' integration' + '\n') + f.write(' ' + f'{template.name}:' + '\n') + f.write(' ' + ' ' + '# If the integration is enabled' + '\n') + f.write(' ' + ' ' + f'enabled: {str(template.enabled).lower()}' + '\n') + f.write(' ' + ' ' + '\n') + for key, value in template.vars.items(): + snake_case_name = key.split('_')[0] + for token in key.split('_'): + if token != snake_case_name: + snake_case_name += token.capitalize() + f.write(' ' + ' ' + f'{snake_case_name}: {value}' + '\n') + f.write(' ' + ' ' + '\n') + #f.write(' ' + '# Configuration for the OpenAI integration' + '\n') + #f.write(' ' + 'openai:' + '\n') + #f.write(' ' + ' ' + '# If the OpenAI integration is enabled' + '\n') + #f.write(' ' + ' ' + 'enabled: true' + '\n') + #f.write(' ' + ' ' + '\n') + #f.write(' ' + ' ' + '# The OpenAI API key' + '\n') + #f.write(' ' + ' ' + 'apiKey: ""' + '\n') + + def write_helmignore(self): + with open('.helmignore', 'w') as f: + f.write('# Ignore the ignore file' + '\n') + f.write('.helmignore' + '\n') + f.write('# Ignore the Helm chart\'s packaged tarball' + '\n') + f.write('*.tgz' + '\n') + f.write('# Ignore git files (In case done in the same directory as code)' + '\n') + f.write('.git' + '\n') + f.write('.gitignore' + '\n') + f.write('# Ignore the README file (In case done in the same directory as code)' + '\n') + f.write('README.md' + '\n') + f.write('# Ignore the requirements file (In case done in the same directory as code)' + '\n') + f.write('requirements.txt' + '\n') + f.write('# Ignore this file (In case done in the same directory as code)' + '\n') + f.write('create-helm-chart.py' + '\n') + + def package(self): + """Package the Helm chart for publishing.""" + + result = subprocess.run(['helm', 'package', '.']) + + if result.returncode != 0: + raise Exception('Failed to package the Helm chart.') + + def push(self, registry: str): + """Push the Helm chart to the Helm remote registry. + + Args: + registry (str): The URL of the Helm remote registry + """ + + if not os.path.exists(f'{self.chartName}-{self.chartVersion}.tgz'): + raise Exception('The Helm chart has not been packaged yet.') + + print(f'Pushing {self.chartName}-{self.chartVersion}.tgz to {registry}') + + subprocess.run(['helm', 'push', f'{self.chartName}-{self.chartVersion}.tgz', f'oci://{registry}']) \ No newline at end of file diff --git a/src/Ingress.py b/src/Ingress.py new file mode 100644 index 0000000..ba4f547 --- /dev/null +++ b/src/Ingress.py @@ -0,0 +1,39 @@ +from .Template import Template + +class Ingress (Template): + def __init__(self, hostname: str): + """A class for creating a/some template(s) related to the Ingress for the app. + + Args: + hostname (str): The hostname that the Ingress will be accessed on. + """ + + super().__init__() + + self.hostname = hostname + + def write(self): + """Write the Ingress template to a file.""" + + with open(f'templates/ingress.yaml', 'w') as f: + f.write('{{- if .Values.ingress.enabled -}}' + '\n') + f.write('apiVersion: networking.k8s.io/v1' + '\n') + f.write('kind: Ingress' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}' + '\n') + f.write(' ' + 'annotations:' + '\n') + f.write(' ' + ' ' + 'nginx.ingress.kubernetes.io/rewrite-target: /' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'rules:' + '\n') + f.write(' ' + '- host: {{ .Values.ingress.host }}' + '\n') + f.write(' ' + ' ' + 'http:' + '\n') + f.write(' ' + ' ' + ' ' + 'paths:' + '\n') + f.write(' ' + ' ' + ' ' + '- path: /' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'pathType: Prefix' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'backend:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'service:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'port:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'number: 80' + '\n') + f.write(' ' + 'ingressClassName: {{ .Values.ingress.class }}' + '\n') + f.write('{{- end -}}') \ No newline at end of file diff --git a/src/MongoDB.py b/src/MongoDB.py new file mode 100644 index 0000000..8f97e14 --- /dev/null +++ b/src/MongoDB.py @@ -0,0 +1,114 @@ +from .NoSQL import NoSQL + +class MongoDB (NoSQL): + def __init__(self, db_name: str, user: str, password: str, tables: dict[str, str], create: bool = True, replica_count: int = 3, tls_enabled: bool = False): + super().__init__('mongodb', db_name, tables, create) + + self.user = user + self.password = password + self.replica_count = replica_count + self.tls_enabled = tls_enabled + + def write(self): + super().write() + + with open('templates/mongo-service-account-database.yaml', 'w') as f: + f.write('{{- if and (eq .Values.nosql.type "mongodb") (.Values.nosql.create) -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: ServiceAccount' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: mongodb-database' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('{{- end -}}') + + with open('templates/mongo-role-database.yaml', 'w') as f: + f.write('{{- if and (eq .Values.nosql.type "mongodb") (.Values.nosql.create) -}}' + '\n') + f.write('apiVersion: rbac.authorization.k8s.io/v1' + '\n') + f.write('kind: Role' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: mongodb-database' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('rules:' + '\n') + f.write(' ' + '- apiGroups:' + '\n') + f.write(' ' + ' ' + '- ""' + '\n') + f.write(' ' + ' ' + 'resources:' + '\n') + f.write(' ' + ' ' + '- secrets' + '\n') + f.write(' ' + ' ' + 'verbs:' + '\n') + f.write(' ' + ' ' + '- get' + '\n') + f.write(' ' + '- apiGroups:' + '\n') + f.write(' ' + ' ' + '- ""' + '\n') + f.write(' ' + ' ' + 'resources:' + '\n') + f.write(' ' + ' ' + '- pods' + '\n') + f.write(' ' + ' ' + 'verbs:' + '\n') + f.write(' ' + ' ' + '- patch' + '\n') + f.write(' ' + ' ' + '- delete' + '\n') + f.write(' ' + ' ' + '- get' + '\n') + f.write('{{- end -}}') + + with open('templates/mongo-role-binding-database.yaml', 'w') as f: + f.write('{{- if and (eq .Values.nosql.type "mongodb") (.Values.nosql.create) -}}' + '\n') + f.write('apiVersion: rbac.authorization.k8s.io/v1' + '\n') + f.write('kind: RoleBinding' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: mongodb-database' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('subjects:' + '\n') + f.write('- kind: ServiceAccount' + '\n') + f.write(' ' + 'name: mongodb-database' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('roleRef:' + '\n') + f.write(' ' + 'kind: Role' + '\n') + f.write(' ' + 'name: mongodb-database' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write(' ' + 'apiGroup: rbac.authorization.k8s.io' + '\n') + f.write('{{- end -}}') + + with open('templates/mongo-credentials-secret.yaml', 'w') as f: + f.write('{{- if eq .Values.nosql.type "mongodb" -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-mongo-credentials' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'user: {{ .Values.nosql.user | b64enc }}' + '\n') + f.write(' ' + 'password: {{ .Values.nosql.password | b64enc }}' + '\n') + f.write(' ' + '{{- if and (.Values.nosql.connectionString) (not .values.nosql.create) }}' + '\n') + f.write(' ' + 'connection-string: {{ .Values.nosql.connectionString | b64enc }}' + '\n') + f.write(' ' + '{{- else if .Values.nosql.create }}' + '\n') + f.write(' ' + 'connection-string: {{ printf "mongodb://%s:%s@%s-mongo-svc.%s.svc.cluster.local:27017/%s?replicaSet=%s-mongo" .Values.nosql.user .Values.nosql.password .Release.Name .Release.Namespace .Values.nosql.name .Release.Name | b64enc }}' + '\n') + f.write(' ' + '{{- end }}' + '\n') + f.write('{{- end -}}') + + with open('templates/mongo.yaml', 'w') as f: + f.write('{{- if and (eq .Values.nosql.type "mongodb") (.Values.nosql.create) -}}' + '\n') + f.write('apiVersion: mongodbcommunity.mongodb.com/v1' + '\n') + f.write('kind: MongoDBCommunity' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-mongo' + '\n') + f.write(' ' + 'namespace: {{ .Release.Namespace }}' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'members: {{ .Values.nosql.replicaCount }}' + '\n') + f.write(' ' + 'type: ReplicaSet' + '\n') + f.write(' ' + 'version: 4.4.0' + '\n') + f.write(' ' + 'security:' + '\n') + f.write(' ' + ' ' + 'authentication:' + '\n') + f.write(' ' + ' ' + ' ' + 'ignoreUnknownUsers: true' + '\n') + f.write(' ' + ' ' + ' ' + 'modes:' + '\n') + f.write(' ' + ' ' + ' ' + '- SCRAM' + '\n') + f.write(' ' + ' ' + 'tls:' + '\n') + f.write(' ' + ' ' + ' ' + 'enabled: {{ .Values.nosql.tls.enabled }}' + '\n') + f.write(' ' + 'readinessProbe:' + '\n') + f.write(' ' + ' ' + 'initialDelaySeconds: 30' + '\n') + f.write(' ' + ' ' + 'periodSeconds: 10' + '\n') + f.write(' ' + 'users:' + '\n') + f.write(' ' + ' ' + '- name: {{ .Values.nosql.user }}' + '\n') + f.write(' ' + ' ' + ' ' + 'db: {{ .Values.nosql.name }}' + '\n') + f.write(' ' + ' ' + ' ' + 'passwordSecretRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-mongo-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'key: password' + '\n') + f.write(' ' + ' ' + ' ' + 'roles:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: readWrite' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'db: {{ .Values.nosql.name }}' + '\n') + f.write(' ' + ' ' + ' ' + 'scramCredentialsSecretName: {{ .Release.Name }}-mongo-scram' + '\n') + f.write('{{- end -}}') \ No newline at end of file diff --git a/src/NoSQL.py b/src/NoSQL.py new file mode 100644 index 0000000..3fbff22 --- /dev/null +++ b/src/NoSQL.py @@ -0,0 +1,29 @@ +from .Template import Template + +class NoSQL (Template): + def __init__(self, type: str, db_name: str, tables: dict[str, dict[str, str]], create: bool = True): + super().__init__() + + self.type = type + self.db_name = db_name + self.tables = tables + self.create = create + + def write(self): + with open('templates/storage-tables-config-map.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: ConfigMap' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-storage-tables' +'\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write('data:' + '\n') + + # Loop over tables dictionary and get the values + for value in self.tables.values(): + snake_case_name = value['name'].split('-')[0] + for token in value['name'].split('-'): + if token != snake_case_name: + snake_case_name += token.capitalize() + + f.write(' ' + value['name'] + ': {{ .Values.tables.' + snake_case_name + ' }}' + '\n') \ No newline at end of file diff --git a/src/OAuth.py b/src/OAuth.py new file mode 100644 index 0000000..181f777 --- /dev/null +++ b/src/OAuth.py @@ -0,0 +1,40 @@ +from .Template import Template + +class OAuth (Template): + def __init__(self, base_app_url: str, app_abbreviation: str, app_name: str, service_name: str, dev_port: str, client_id: str, client_secret: str): + """A class for creating a/some template(s) related to OAuth implementation.""" + + self.base_app_url = base_app_url + self.app_abbreviation = app_abbreviation + self.app_name = app_name + self.service_name = service_name + self.dev_port = dev_port + self.client_id = client_id + self.client_secret = client_secret + + def write(self): + with open('templates/oauth-credentials-config-map.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: ConfigMap' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-oauth-credentials' + '\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'base-app-url: {{ .Values.oauth.baseAppUrl }}' + '\n') + f.write(' ' + 'app-abbreviation: {{ .Values.oauth.appAbbreviation }}' + '\n') + f.write(' ' + 'app-name: {{ .Values.oauth.appName }}' + '\n') + f.write(' ' + 'service-name: {{ .Values.oauth.serviceName }}' + '\n') + f.write(' ' + 'dev-port: {{ .Values.oauth.devPort | quote }}' + '\n') + f.write(' ' + 'client-id: {{ .Values.oauth.clientId | quote }}' + '\n') + + with open('templates/oauth-client-secret.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-oauth-client-secret' + '\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'client-secret: {{ .Values.oauth.clientSecret | b64enc }}' + '\n') \ No newline at end of file diff --git a/src/Redis.py b/src/Redis.py new file mode 100644 index 0000000..5e5b36e --- /dev/null +++ b/src/Redis.py @@ -0,0 +1,98 @@ +from .Cache import Cache + +class Redis (Cache): + def __init__(self, password: str, create: bool = True, hostName: str = '{{ .Release.Name }}-redis', replicaCount: int = 1, port: str = '6379', tls_enabled: bool = False, tls_port: str = '6380', image: dict[str, str] = {}): + """A class for creating a/some template(s) related to a Redis server. + + Args: + password (str): The password to use to access the Redis instance. + create (bool, Optional): Whether or not to create the Redis instance as part of the Helm Chart. Default True + hostName (str, Optional): The hostname of the Redis instance. Default '{{ .Release.Name }}-redis' + replicaCount (int, Optional): The number of replicas of the Redis instance. Default 1 + port (str, Optional): The port of the Redis instance. Default '6379' + tls_enabled (bool, Optional): Whether or not to enable TLS for the Redis instance. Default False + tls_port (str, Optional): The port of the Redis instance for TLS. Default '6380' + image (dict[str, str], Optional): The image of the Redis instance. Default {} + """ + + super().__init__(password, hostName, port, create) + + self.replicaCount = replicaCount + self.tls_enabled = tls_enabled + self.tls_port = tls_port + self.image = image + + def write(self): + # Call parent class's method/function to write the generic cache templates + super().write_generic_cache_templates('redis', '{{ .Release.Name }}-redis') + + # Create the Redis service file + with open('templates/redis-service.yaml', 'w') as f: + f.write('{{- if and (.Values.cache.type "redis") (.Values.cache.create) -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: Service' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-redis' + '\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: redis' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'ports:' + '\n') + f.write(' ' + ' ' + '- port: {{ .Values.cache.port }}' + '\n') + f.write(' ' + ' ' + ' ' + 'targetPort: {{ .Values.cache.port }}' + '\n') + f.write(' ' + 'selector:' + '\n') + f.write(' ' + ' ' + 'app: redis' + '\n') + f.write(' ' + 'type: ClusterIP' + '\n') + f.write('{{- end -}}') + + + # Create the Redis deployment file + with open('templates/redis-deployment.yaml', 'w') as f: + f.write('{{- if and (.Values.cache.type "redis") (.Values.cache.create) -}}' + '\n') + f.write('apiVersion: apps/v1' + '\n') + f.write('kind: Deployment' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-redis' + '\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: redis' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'replicas: {{ .Values.cache.replicaCount }}' + '\n') + f.write(' ' + 'selector:' + '\n') + f.write(' ' + ' ' + 'matchLabels:' + '\n') + f.write(' ' + ' ' + ' ' + 'app: redis' + '\n') + f.write(' ' + 'template:' + '\n') + f.write(' ' + ' ' + 'metadata:' + '\n') + f.write(' ' + ' ' + ' ' + 'labels:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + 'app: redis' + '\n') + f.write(' ' + ' ' + 'spec:' + '\n') + f.write(' ' + ' ' + ' ' + 'containers:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: redis' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'image: {{ .Values.cache.image.repository | default "bitnami/redis" }}:{{ .Values.cache.image.tag | default "7.0.5" }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'ports:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '- containerPort: {{ .Values.cache.port }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '{{- if .Values.cache.tls.enabled }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '- containerPort: {{ .Values.cache.tls.port }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '{{- end }}' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'env:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '- name: ALLOW_EMPTY_PASSWORD' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'value: "false"' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '- name: REDIS_PASSWORD' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'valueFrom:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'secretKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-cache-credentials' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: password' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '- name: REDIS_DISABLE_COMMANDS' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'value: "FLUSHDB,FLUSHALL"' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '# TLS configuration' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '#- name: REDIS_TLS_ENABLED' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '# value: "{{ .Values.cache.tls.enabled }}"' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '#- name: REDIS_TLS_AUTH_CLIENTS' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '# value: "yes"' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '#- name: REDIS_TLS_PORT_NUMBER' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '# value: "{{ .Values.cache.tls.port }}"' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'volumeMounts:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '- name: redis-data' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'mountPath: /bitnami/redis' + '\n') + f.write(' ' + ' ' + ' ' + 'volumes:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + '- name: redis-data' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + 'emptyDir: {}' + '\n') + f.write('{{- end -}}') \ No newline at end of file diff --git a/src/Service.py b/src/Service.py new file mode 100644 index 0000000..920d01e --- /dev/null +++ b/src/Service.py @@ -0,0 +1,23 @@ +from .Template import Template + +class Service (Template): + def __init__(self): + """A class for creating a/some template(s) related to the Service for the app.""" + + super().__init__() + + def write(self): + """Write the Service template to a file.""" + + with open(f'templates/service.yaml', 'w') as f: + f.write('apiVersion: v1' + '\n') + f.write('kind: Service' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}' + '\n') + f.write('spec:' + '\n') + f.write(' ' + 'selector:' + '\n') + f.write(' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write(' ' + 'ports:' + '\n') + f.write(' ' + ' ' + '- protocol: TCP' + '\n') + f.write(' ' + ' ' + ' ' + 'port: 80' + '\n') + f.write(' ' + ' ' + ' ' + 'targetPort: {{ .Values.container.port }}' + '\n') \ No newline at end of file diff --git a/src/Template.py b/src/Template.py new file mode 100644 index 0000000..cb9928c --- /dev/null +++ b/src/Template.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + +class Template(ABC): + """An abstract class for creating a/some template(s).""" + + @abstractmethod + def write(self): + """Write the template to a file.""" + + pass \ No newline at end of file diff --git a/src/ThirdPartyService.py b/src/ThirdPartyService.py new file mode 100644 index 0000000..63996b3 --- /dev/null +++ b/src/ThirdPartyService.py @@ -0,0 +1,30 @@ +from .Template import Template + +class ThirdPartyService (Template): + def __init__(self, name: str, enabled: bool, **vars: str): + super().__init__() + + self.name = name + self.enabled = enabled + self.vars = vars + + def write(self): + with open(f'templates/{self.name}-secret.yaml', 'w') as f: + f.write('{{- if .Values.thirdParty.' + self.name + '.enabled -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-' + self.name + '-secret' + '\n') + f.write(' ' + 'labels:' + '\n') + f.write(' ' + ' ' + 'app: {{ .Release.Name }}' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + for key, value in self.vars.items(): + snake_case_name = key.split('_')[0] + for token in key.split('_'): + if token != snake_case_name: + snake_case_name += token.capitalize() + + f.write(' ' + key.replace('_', '-') + ': {{ .Values.thirdParty.' + self.name + '.' + snake_case_name + ' | b64enc }}' + '\n') + #f.write(' ' + 'api-key: {{ .Values.thirdParty.openai.apiKey | b64enc }}' + '\n') + f.write('{{- end -}}' + '\n') \ No newline at end of file