From b7f67bec68c399662fce5073eae43c357f2bd206 Mon Sep 17 00:00:00 2001 From: Alan Bridgeman Date: Wed, 12 Feb 2025 05:54:59 -0600 Subject: [PATCH] Added code around using Azure Table Storage (which already existed in some ways) + refactored particularly how the values.yaml file is generated --- src/AzureTableStorage.py | 66 ++++ src/Deployment.py | 4 +- src/HelmChart.py | 767 +++++++++++++++++++++++++-------------- src/NoSQL.py | 9 + 4 files changed, 568 insertions(+), 278 deletions(-) create mode 100644 src/AzureTableStorage.py diff --git a/src/AzureTableStorage.py b/src/AzureTableStorage.py new file mode 100644 index 0000000..15583f3 --- /dev/null +++ b/src/AzureTableStorage.py @@ -0,0 +1,66 @@ +from .NoSQL import NoSQL + +class AzureTableStorage (NoSQL): + def __init__(self, db_name: str, key: str, tables: dict[str, dict[str, str]]): + """Use of Azure Table Storage (Azure Storage Account) as a NoSQL storage system + + Args: + db_name (str): The name of the Azure Storage Account to connect to + key (str): The key to use to connect to the Azure Storage Account + tables (dict[str, dict[str, str]]): A dictionary of table names and their details + """ + + # Call the parent class constructor + # Note, the following: + # - The type is set to 'azure' because we are using Azure Table Storage + # - The create parameter is set to False because an Azure Storage Account can't be created/provisioned as part of a Helm deployment + super().__init__('azure', db_name, tables, False) + + self.key = key + + def write_config_map(self, filename: str = 'azure-tables-configmap.yaml'): + """Writes the configmap file for the Azure Table Storage + + This configmap contains non-sensitive information about the Azure Table Storage. + Such as the name of the Azure Storage Account to connect to. + + Args: + filename (str, optional): The name of the file to write to. Defaults to 'azure-tables-configmap.yaml'. + """ + + with open(f'templates/{filename}', 'w') as f: + f.write('{{- if eq .Values.nosql.type "azure" -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: ConfigMap' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-azure-tables-config' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'name: {{ .Values.nosql.name }}' + '\n') + f.write('{{- end -}}' + '\n') + + def write_secret(self, filename: str = 'azure-tables-credentials-secret.yaml'): + """Writes the secret file for the Azure Table Storage + + This secret contains sensitive information about the Azure Table Storage. + Such as the key to use to connect to the Azure Storage Account. + + Args: + filename (str, optional): The name of the file to write to. Defaults to 'azure-tables-credentials-secret.yaml'. + """ + + with open(f'templates/{filename}', 'w') as f: + f.write('{{- if eq .Values.nosql.type "azure" -}}' + '\n') + f.write('apiVersion: v1' + '\n') + f.write('kind: Secret' + '\n') + f.write('metadata:' + '\n') + f.write(' ' + 'name: {{ .Release.Name }}-azure-tables-credentials' + '\n') + f.write('type: Opaque' + '\n') + f.write('data:' + '\n') + f.write(' ' + 'key: {{ .Values.nosql.key | b64enc }}' + '\n') + f.write('{{- end -}}' + '\n') + + def write(self): + """Writes the needed template files for the Azure Table Storage""" + + self.write_config_map() + self.write_secret() \ No newline at end of file diff --git a/src/Deployment.py b/src/Deployment.py index 58e5abc..3a34059 100644 --- a/src/Deployment.py +++ b/src/Deployment.py @@ -176,8 +176,8 @@ class Deployment (Template): 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(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'configMapKeyRef:' + '\n') + f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'name: {{ .Release.Name }}-azure-tables-config' + '\n') f.write(' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + 'key: name' + '\n') f.write(' ' + ' ' + ' ' + ' ' + '{{- end }}' + '\n') f.write(' ' + ' ' + ' ' + ' ' + '# NoSQL Table Names' + '\n') diff --git a/src/HelmChart.py b/src/HelmChart.py index d7ee384..057e555 100644 --- a/src/HelmChart.py +++ b/src/HelmChart.py @@ -5,6 +5,7 @@ from .Ingress import Ingress from .Database import Database from .NoSQL import NoSQL from .MongoDB import MongoDB +from .AzureTableStorage import AzureTableStorage from .Redis import Redis from .OAuth import OAuth from .ThirdPartyService import ThirdPartyService @@ -45,7 +46,10 @@ class HelmChart: template.write() def write_yaml(self): - """Write the Chart.yaml file for the Helm chart.""" + """Write the Chart.yaml file for the Helm chart. + + This provides the metadata about the Helm chart itself. + """ with open('Chart.yaml', 'w') as f: f.write(f'apiVersion: {self.apiVersion}' + '\n') @@ -62,309 +66,520 @@ class HelmChart: 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.""" + def create_replicas_section_of_values_yaml(self) -> str: + """Create the replicas section of the `values.yaml` file for the Helm chart. - # Get the Deployment and Ingress templates from the templates provided + Unlike most of the other `create_..._section_of_values_yaml` methods, + the "replicas" section isn't an actual "section" in the sense that it doesn't have subfields of a `replica` field. + Instead, it's just a (or a set of) fields straight under the root of the `values.yaml` file. + + Particularly, at time of writing the `replicaCount` field which is how many replicas of the app to run. + + Returns: + str: The replicas section of the `values.yaml` file + """ + + output = '' + + # Get the Deployment templates from the templates provided deployment_template = next(template for template in self.templates if isinstance(template, Deployment)) + + output += '# The number of instances (replicas) of the app to run' + '\n' + output += f'replicaCount: {deployment_template.replica_count}' + '\n' + output += '\n' + + return output + + def create_image_section_of_values_yaml(self) -> str: + """Create the image section of the `values.yaml` file for the Helm chart. + + The image section is used to define the image that the app will use. + + Returns: + str: The image section of the `values.yaml` file + """ + + output = '' + + # Get the Deployment templates from the templates provided + deployment_template = next(template for template in self.templates if isinstance(template, Deployment)) + + output += 'image:' + '\n' + output += ' ' + '# The repository of the image to use for the app' + '\n' + output += ' ' + '# Should be in the format `/`' + '\n' + output += ' ' + f'repository: "{deployment_template.image_repository}"' + '\n' + output += ' ' + '# 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' + output += ' ' + '# Ex. v1.0.0' + '\n' + output += ' ' + f'tag: "{deployment_template.image_tag}"' + '\n' + output += ' ' + '# How often the image should be pulled. The possible values are "Always", "Never", and "IfNotPresent"' + '\n' + output += ' ' + '# It\'s recommended for production to use "IfNotPresent" to avoid pulling the image every time the pod starts' + '\n' + output += ' ' + '# Though, for development, "Always" is recommended to ensure the latest changes are being tested' + '\n' + output += ' ' + f'pullPolicy: "{deployment_template.image_pull_policy}"' + '\n' + output += '\n' + + return output + + def create_container_section_of_values_yaml(self) -> str: + """Create the container section of the `values.yaml` file for the Helm chart. + + The container section is used to define the environment that the container is running in. + Ex. the environment type (development, production, etc...) and the port that the container listens on. + + Returns: + str: The container section of the `values.yaml` file + """ + + output = '' + + # Get the Deployment templates from the templates provided + deployment_template = next(template for template in self.templates if isinstance(template, Deployment)) + + output += 'container:' + '\n' + output += ' ' + '# The port that the container listens on (Ex. 8080)' + '\n' + output += ' ' + f'port: {deployment_template.port}' + '\n' + output += ' ' + '\n' + output += ' ' + '# The environment that the container is running in (Ex. development, production, etc...)' + '\n' + output += ' ' + '# This is used for the NODE_ENV environment variable' + '\n' + output += ' ' + f'env: "{deployment_template.env}"' + '\n' + output += '\n' + + return output + + def create_ingress_section_of_values_yaml(self) -> str: + """Create the ingress section of the `values.yaml` file for the Helm chart. + + The ingress section is used to define the ingress resource that the app will use. + That is how the app will be accessed from outside the cluster. + + Returns: + str: The ingress section of the `values.yaml` file + """ + + output = '' + + # Get the Ingress templates from the templates provided ingress_template = next(template for template in self.templates if isinstance(template, Ingress)) + output += 'ingress:' + '\n' + output += ' ' + '# We want an ingress resource if we are deploying to a cluster that has a ingress controller/load balancer' + '\n' + output += ' ' + '# This includes most public cloud providers like EKS, GKE, and AKS' + '\n' + output += ' ' + 'enabled: true' + '\n' + output += ' ' + '# The DNS Name (Ex. app.example.com) where the app will be accessible' + '\n' + output += ' ' + f'host: "{ingress_template.hostname}"' + '\n' + output += ' ' + '# 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' + output += ' ' + 'class: nginx' + '\n' + output += '\n' + + return output + + def create_deployment_extra_vars_section_of_values_yaml(self) -> str: + """Create the extra environment variables for the deployment section of the `values.yaml` file for the Helm chart. + + Somewhat similar to the "replicas" section, the "extra environment variables" aren't a real "section" in the sense that they don't have subfields of an `extraEnvVars` field. + + The extra environment variables are used to define additional environment variables that the deployment will use. + This is useful for providing extra configuration to the app that is being deployed. + + Returns: + str: The extra environment variables section of the `values.yaml` file + """ + + output = '' + + # Get the Deployment templates from the templates provided + deployment_template = next(template for template in self.templates if isinstance(template, Deployment)) + + 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: + output += f'# {value["description"]}' + '\n' + + # Because the name given within the value in the dictionary is intended as the unique name of the configmap/secret that contains the value + # It usually contains a reference to the Helm release name. + # However, here we want the name without this component so we do a string replacement to remove it. + var_name = value["name"].replace('{{ .Release.Name }}-', '') + + # We want to convert the name to camelCase because this is our convention for `values.yaml` keys + camel_case_name = var_name.split('-')[0] + for token in var_name.split('-'): + if token != camel_case_name: + camel_case_name += token.capitalize() + + output += f'{camel_case_name}: "{value["value"]}"' + '\n' + + # For readability of the `values.yaml` file we put an extra line between each extra deployment environment variable + output += '\n' + + return output + + def create_oauth_section_of_values_yaml(self) -> str: + """Create the OAuth section of the `values.yaml` file for the Helm chart. + + The OAuth section is used to define the OAuth configuration that the app will use. + This is useful for providing OAuth configuration to the app that is being deployed. + + Note, this assumes the app uses the Bridgeman Accessible OAuth implementation etc... + + Returns: + str: The OAuth section of the `values.yaml` file + """ + + output = '' + + # Get the OAuth template from the templates provided + oauth_template = next(template for template in self.templates if isinstance(template, OAuth)) + + output += '# Configuration for using OAuth within the app' + '\n' + output += 'oauth:' + '\n' + output += ' ' + f'baseAppUrl: "{oauth_template.base_app_url}"' + '\n' + output += ' ' + f'appAbbreviation: "{oauth_template.app_abbreviation}"' + '\n' + output += ' ' + f'appName: "{oauth_template.app_name}"' + '\n' + output += ' ' + f'serviceName: "{oauth_template.service_name}"' + '\n' + output += ' ' + f'devPort: "{oauth_template.dev_port}"' + '\n' + output += '\n' + + return output + + def create_database_section_of_values_yaml(self) -> str: + """Create the Database section of the `values.yaml` file for the Helm chart. + + The Database section is used to define the database configuration that the app will use. + + Returns: + str: The Database section of the `values.yaml` file + """ + + output = '' + + # Get the Database template from the templates provided + database_template = next(template for template in self.templates if isinstance(template, Database)) + + output += '# Configuration for the relational database' + '\n' + output += 'database:' + '\n' + output += ' ' + '# The type of the relational database that is used.' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + '# The following table lists the possible values for this field:' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + '# | Value | Description |' + '\n' + output += ' ' + '# | ---------- | ------------------------------------------ |' + '\n' + output += ' ' + '# | `postgres` | Uses PostgreSQL as the relational database |' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + '# Note, for use of `postgres`, it uses a [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + f'type: "{database_template.type}"' + '\n' + output += ' ' + '\n' + output += ' ' + '# If set to `true`, the database will be created as part of the deployment' + '\n' + output += ' ' + '# This uses the [`postgres-controller` CRD](https://github.com/AlanBridgeman/postgres-controller) to create the database' + '\n' + output += ' ' + f'create: {str(database_template.create).lower()}' + '\n' + output += ' ' + '\n' + output += ' ' + '# The host that the database is located on ' + '\n' + output += ' ' + f'host: "{database_template.host}"' + '\n' + output += ' ' + '\n' + output += ' ' + '# The name of the database to be used' + '\n' + output += ' ' + f'name: "{database_template.name}"' + '\n' + output += ' ' + '\n' + output += ' ' + '# The user that is used to access the database' + '\n' + output += ' ' + f'user: "{database_template.user}"' + '\n' + output += ' ' + '\n' + output += ' ' + '# The password that is used to access the database' + '\n' + output += ' ' + f'password: "{database_template.password}"' + '\n' + output += ' ' + '\n' + output += ' ' + '# The port that the database listens on' + '\n' + output += ' ' + f'#port: {database_template.port}' + '\n' + output += ' ' + '\n' + output += ' ' + '# Allows for distinguishing between multiple database instances/servers' + '\n' + output += ' ' + f'#instance_id: "{database_template.instance_id}"' + '\n' + output += '\n' + + return output + + def create_nosql_section_of_values_yaml(self) -> str: + """Create the NoSQL section of the `values.yaml` file for the Helm chart. + + The NoSQL section is used to define the NoSQL storage configuration that the app will use. + + Returns: + str: The NoSQL section of the `values.yaml` file + """ + + output = '' + + nosql_template = next(template for template in self.templates if isinstance(template, NoSQL)) + + output += '# Configuration the NoSQL database' + '\n' + output += '# Within the parlance of the system these are often called "properties" databases (and store less structured data)' + '\n' + output += 'nosql:' + '\n' + output += ' ' + '# Determines the type of NoSQL storage that is used' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + '# The following table lists the possible values for this field:' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + '# | Value | Description |' + '\n' + output += ' ' + '# | --------- | ------------------------------------------------------------------------------------------ |' + '\n' + output += ' ' + '# | `mongodb` | Uses MongoDB as the NoSQL database for the default account properties database |' + '\n' + output += ' ' + '# | `azure` | Uses Azure Table Storage as the NoSQL database for the default account properties database |' + '\n' + output += ' ' + '# ' + '\n' + output += ' ' + f'type: {nosql_template.type}' + '\n' + output += ' ' + '\n' + + output += ' ' + '# If to create a resource as part of the deployment process' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `mongodb`' + '\n' + output += ' ' + '# This uses the [MongoDBCommunity CRD](https://github.com/mongodb/mongodb-kubernetes-operator) to create the resource' + '\n' + output += ' ' + f'create: {str(nosql_template.create).lower()}' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The number of replicas/members as part of the Mongo deployment' + '\n' + output += ' ' + '# See the `member` parameter of the [MongoDBCommunity CRD](https://github.com/mongodb/mongodb-kubernetes-operator) for more information' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `mongodb` and `create` is set to `true`' + '\n' + if isinstance(nosql_template, MongoDB): + output += ' ' + f'replicaCount: {nosql_template.replica_count}' + '\n' + else: + output += ' ' + '#replicaCount: ' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The TLS configuration for the connection to the NoSQL database' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `mongodb` and `create` is set to `true`' + '\n' + output += ' ' + 'tls:' + '\n' + output += ' ' + ' ' + '# If to use TLS for the connection to the NoSQL database' + '\n' + if isinstance(nosql_template, MongoDB): + output += ' ' + ' ' + f'enabled: {str(nosql_template.tls_enabled).lower()}' + '\n' + else: + output += ' ' + ' ' + 'enabled: ' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The connection string used to access the NoSQL database' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `mongodb` and `create` is set to `false`' + '\n' + output += ' ' + '# Should be in the following format: `mongodb://:`' + '\n' + output += ' ' + '#connectionString: "mongodb://mongo.example.com:27017"' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The key used to access the NoSQL database' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `azure`' + '\n' + if isinstance(nosql_template, AzureTableStorage): + output += ' ' + f'key: "{nosql_template.key}"' + '\n' + else: + output += ' ' + '#key: ""' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The name of the NoSQL database' + '\n' + output += ' ' + f'name: "{nosql_template.db_name}"' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The username used to access the NoSQL database' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `mongodb`' + '\n' + if isinstance(nosql_template, MongoDB): + output += ' ' + f'user: "{nosql_template.user}"' + '\n' + else: + output += ' ' + 'user: ""' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The password used to access the NoSQL database' + '\n' + output += ' ' + '# ONLY relevant if `type` is set to `mongodb`' + '\n' + if isinstance(nosql_template, MongoDB): + output += ' ' + f'password: "{nosql_template.password}"' + '\n' + else: + output += ' ' + 'password: ""' + '\n' + output += '\n' + + output += '# Configurable NoSQL information groupings' + '\n' + output += '# For Azure Table Storage these are table names' + '\n' + output += '# For MongoDB these are collection names' + '\n' + output += 'tables:' + '\n' + + for value in nosql_template.tables.values(): + camel_case_name = value['name'].split('-')[0] + for token in value['name'].split('-'): + if token != camel_case_name: + camel_case_name += token.capitalize() + + output += ' ' + f'{camel_case_name}: "{value["value"]}"' + '\n' + + output += '\n' + + return output + + def create_cache_section_of_values_yaml(self) -> str: + """Create the Cache section of the `values.yaml` file for the Helm chart. + + The Cache section is used to define the cache (usually Redis) configuration that the app will use. + + Returns: + str: The Cache section of the `values.yaml` file + """ + + output = '' + + # Get the Redis template from the templates provided + redis_template = next(template for template in self.templates if isinstance(template, Redis)) + + output += '# Configuration for cache server' + '\n' + output += 'cache:' + '\n' + output += ' ' + 'type: "redis"' + '\n' + output += ' ' + '\n' + + output += ' ' + '# If to create a Redis instance/resource as part of the deployment process' + '\n' + output += ' ' + f'create: {str(redis_template.create).lower()}' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The image to use for the Redis instance' + '\n' + output += ' ' + '# 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: + output += ' ' + 'image' + '\n' + + # Loop through the image dictionary and write the image values + for key, value in redis_template.image.items(): + output += ' ' + ' ' + f'{key}: "{value}"' + '\n' + else: + output += ' ' + 'image: {}' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The number of replicas of the Redis instance' + '\n' + output += ' ' + '# ONLY relevant if `create` is set to `true`' + '\n' + if redis_template.create: + output += ' ' + f'replicaCount: {redis_template.replicaCount}' + '\n' + else: + output += ' ' + '#replicaCount: ' + '\n' + output += ' ' + '\n' + + output += ' ' + '# Hostname of the Redis server' + '\n' + output += ' ' + '# ONLY relevant if `create` is set to `false`' + '\n' + if redis_template.create: + output += ' ' + '#hostName: ""' + '\n' + else: + output += ' ' + f'hostName: "{redis_template.hostName}"' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The password to use for the Redis server' + '\n' + output += ' ' + f'password: "{redis_template.password}"' + '\n' + output += ' ' + '\n' + + output += ' ' + '# The port of the Redis server' + '\n' + output += ' ' + f'port: "{redis_template.port}"' + '\n' + output += ' ' + '\n' + + output += ' ' + '# Redis TLS Configurations' + '\n' + output += ' ' + 'tls:' + '\n' + output += ' ' + ' ' + '# If TLS is enabled for the Redis instance' + '\n' + output += ' ' + ' ' + f'enabled: {str(redis_template.tls_enabled).lower()}' + '\n' + output += ' ' + ' ' + '\n' + output += ' ' + ' ' + '# The port of the Redis instance for TLS' + '\n' + output += ' ' + ' ' + '# ONLY relevant if `tls.enabled` is set to `true`' + '\n' + if redis_template.tls_enabled: + output += ' ' + ' ' + f'port: "{redis_template.tls_port}"' + '\n' + else: + output += ' ' + ' ' + '#port: ""' + '\n' + output += ' ' + '\n' + + return output + + def create_third_party_service_section_of_values_yaml(self) -> str: + """Create the Third Party Service section of the `values.yaml` file for the Helm chart. + + The Third Party Service section is used to define any third-party service configurations that the app will use. + Ex. OpenAI, Stripe, etc... + + Returns: + str: The Third Party Service section of the `values.yaml` file + """ + + output = '' + + output += '# Configurations for integration with third-party services' + '\n' + output += 'thirdParty:' + '\n' + for template in self.templates: + if isinstance(template, ThirdPartyService): + output += ' ' + '# Configurations for the ' + template.name.capitalize() + ' integration' + '\n' + output += ' ' + f'{template.name}:' + '\n' + output += ' ' + ' ' + '# If the integration is enabled' + '\n' + output += ' ' + ' ' + f'enabled: {str(template.enabled).lower()}' + '\n' + output += ' ' + ' ' + '\n' + for key, value in template.vars.items(): + camel_case_name = key.split('_')[0] + for token in key.split('_'): + if token != camel_case_name: + camel_case_name += token.capitalize() + + output += ' ' + ' ' + f'{camel_case_name}: {value}' + '\n' + output += ' ' + ' ' + '\n' + + def write_values_yaml(self): + """Write the `values.yaml` file for the Helm chart. + + Note, that this generates a `values.yaml` file that is specific to the templates provided. + This means, that it's generally better for usage than distribution. + + In other words, because it's likely it could contain sensitive information, it's not recommended to distribute this file (or include it in git etc...) + """ + 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') + # replicas section (mostly just `replicaCount` but...) + f.write(self.create_replicas_section_of_values_yaml()) - 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') + # image section + f.write(self.create_image_section_of_values_yaml()) - 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') + # container section + f.write(self.create_container_section_of_values_yaml()) - 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') + # ingress section + f.write(self.create_ingress_section_of_values_yaml()) - 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('\n') + # Add the extra environment variables for the deployment to the `values.yaml` file + f.write(self.create_deployment_extra_vars_section_of_values_yaml()) # 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') + f.write(self.create_oauth_section_of_values_yaml()) # 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') + f.write(self.create_database_section_of_values_yaml()) # 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('\n') + f.write(self.create_nosql_section_of_values_yaml()) - # If a Redis template is included in the provided templates than we want to include the appropriate section to the values.yaml file. + # 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(self.create_cache_section_of_values_yaml()) - 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') + # If any Third Party Service templates are included in the provided templates than we want to include the appropriate section to the `values.yaml` file. + if any(isinstance(template, ThirdPartyService) for template in self.templates): + f.write(self.create_third_party_service_section_of_values_yaml()) def write_helmignore(self): + """Write the .helmignore file for the Helm chart. + + This file is used to ignore files that are not needed in the Helm chart. + This makes the chart smaller and more efficient. + """ + 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') + + if os.path.exists('.git'): + f.write('# Ignore git files (In case done in the same directory as code)' + '\n') + f.write('.git' + '\n') + + if os.path.exists('.gitignore'): + f.write('.gitignore' + '\n') + + if os.path.exists('README.md'): + f.write('# Ignore the README file (In case done in the same directory as code)' + '\n') + f.write('README.md' + '\n') + + if os.path.exists('requirements.txt'): + f.write('# Ignore the requirements file (In case done in the same directory as code)' + '\n') + f.write('requirements.txt' + '\n') + + if os.path.exists('create-helm-chart.py'): + 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.""" diff --git a/src/NoSQL.py b/src/NoSQL.py index 3fbff22..682a70e 100644 --- a/src/NoSQL.py +++ b/src/NoSQL.py @@ -2,6 +2,15 @@ from .Template import Template class NoSQL (Template): def __init__(self, type: str, db_name: str, tables: dict[str, dict[str, str]], create: bool = True): + """Use of a NoSQL storage system + + Args: + type (str): The type of NoSQL storage to use + db_name (str): The name of the NoSQL storage to connect to + tables (dict[str, dict[str, str]]): A dictionary of NoSQL tables/collections + create (bool, optional): Whether to create the NoSQL resources as part of the Helm deployment. Defaults to True. + """ + super().__init__() self.type = type