diff --git a/Dockerfile b/Dockerfile index 6614be6..a0a1bf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,35 @@ -# Need to clean this up - had to move to ubuntu:20.04 because -# weasyprint would not properly show SVG icons under -# python:3.8-slim-buster. Using ubuntu increases the image size -# by 250 MB which is terrible. Need to get weasyprint working -# on python image - -#FROM python:3.8-slim-buster AS builder +# Using Ubuntu 20.04 as the base image FROM ubuntu:20.04 AS builder + +# Set noninteractive environment to avoid prompts during build ENV DEBIAN_FRONTEND=noninteractive -# Builder stage dependencies aren't needed by the app at runtime -RUN apt-get update && apt-get install -y \ - libpq-dev \ - python3-pip \ - gcc +# Install dependencies for the build stage +RUN apt-get update && apt-get install -y libpq-dev python3-pip gcc \ + && pip install --upgrade pip setuptools wheel + +# Copy only the requirements.txt to install Python dependencies COPY requirements.txt . -RUN pip install --upgrade pip setuptools wheel RUN pip install -r requirements.txt -#FROM python:3.8-slim-buster AS app +# Start the second stage for the actual application FROM ubuntu:20.04 AS app + +# Set noninteractive environment ENV DEBIAN_FRONTEND=noninteractive +# Set the working directory inside the container WORKDIR /app -#RUN apt-get update && apt-get install -y libpq5 python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libpangocairo-1.0-0 \ + +# Install runtime dependencies RUN apt-get update && apt-get install -y libpq5 python3.8 weasyprint=51-2 \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* + +# Copy installed Python packages from builder stage COPY --from=builder /usr/local /usr/local/ + +# Copy the application source code to the container COPY . . + +# Define the command to run the application CMD ["/bin/bash", "run.sh"] diff --git a/README.md b/README.md index 422ffdd..6ec16db 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,53 @@ You can generate an API token by viewing the following route in your browsers And here is how you use the token to authenticate (curl as an example) + +## Deployment Instructions + +This section provides detailed instructions for deploying the Gapps application using Helm, a package manager for Kubernetes. + +### Prerequisites +Before proceeding with the deployment, ensure you have the following prerequisites met: +- A Kubernetes cluster set up and running. +- Helm installed on your local machine. +- Access to the Kubernetes cluster with appropriate permissions. + +### Deployment Steps +Follow these steps to deploy the Gapps application using Helm: + +1. **Add the Helm Repository** (if your chart is hosted on a Helm repository): + ```shell + helm repo add + helm repo update + ``` + +2. **Deploy the Helm Chart**: + Navigate to the directory containing the `gapps-chart` and run the following command: + ```shell + helm upgrade --install gapps ./gapps-chart -f ./gapps-chart/values.yaml + ``` + Replace `gapps` with your desired release name. Adjust the values in `values.yaml` as necessary for your environment. + +3. **Verify the Deployment**: + Check the status of the deployment: + ```shell + helm list + kubectl get pods + ``` + Ensure that all the pods are running and the application is deployed successfully. + +### Post-Deployment Configuration +After deploying the application, you might need to perform additional configuration steps, such as setting up ingress controllers, configuring persistent storage, or other environment-specific settings. + +### Troubleshooting Tips +If you encounter issues during the deployment, consider the following tips: +- Check the Helm chart's configuration in `values.yaml` for any errors or misconfigurations. +- Use `kubectl describe` to get more information about the pods and identify any issues. +- Refer to the Helm and Kubernetes documentation for more detailed troubleshooting guidelines. + +For further assistance, please refer to the [FAQ section](#faq) or raise an issue in the project's GitHub repository. + + ``` TOKEN="TOKEN HERE" curl /api/v1/tenants -H "token: $TOKEN" diff --git a/docker-compose.yml b/docker-compose.yml index 4b1ac76..642882a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ version: '3' services: gapps: container_name: gapps + build: + context: . image: bmarsh13/gapps:3.5.3 depends_on: - postgres @@ -26,6 +28,8 @@ services: - GUNICORN_WORKERS=2 gapps-worker: container_name: gapps-worker + build: + context: . image: bmarsh13/gapps:3.5.3 depends_on: - postgres diff --git a/gapps-chart/Chart.yaml b/gapps-chart/Chart.yaml new file mode 100644 index 0000000..51a6da2 --- /dev/null +++ b/gapps-chart/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: gapps-chart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "3.5.3" +icon: https://example.com/icon.png \ No newline at end of file diff --git a/gapps-chart/templates/gapps-configmap.yaml b/gapps-chart/templates/gapps-configmap.yaml new file mode 100644 index 0000000..03fa388 --- /dev/null +++ b/gapps-chart/templates/gapps-configmap.yaml @@ -0,0 +1,8 @@ +{{- if .Values.gapps.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: gapps-config +data: + {{- toYaml .Values.gapps.config | nindent 2 }} +{{- end }} diff --git a/gapps-chart/templates/gapps-deployment.yaml b/gapps-chart/templates/gapps-deployment.yaml new file mode 100644 index 0000000..c128dea --- /dev/null +++ b/gapps-chart/templates/gapps-deployment.yaml @@ -0,0 +1,35 @@ +{{- if .Values.gapps.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gapps +spec: + replicas: {{ .Values.gapps.replicaCount }} + selector: + matchLabels: + app: gapps + template: + metadata: + labels: + app: gapps + spec: + imagePullSecrets: + - name: {{ index .Values.commonImage.imagePullSecrets 0 "name" }} + containers: + - name: gapps + image: {{ .Values.commonImage.repository }}:{{ .Values.commonImage.tag }} + ports: + - containerPort: {{ .Values.gapps.service.port }} + envFrom: + - secretRef: + name: {{ .Values.gapps.secretName }} + - configMapRef: + name: {{ .Values.gapps.configMapName }} + resources: + requests: + memory: {{ .Values.gapps.resources.requests.memory }} + cpu: {{ .Values.gapps.resources.requests.cpu }} + limits: + memory: {{ .Values.gapps.resources.limits.memory }} + cpu: {{ .Values.gapps.resources.limits.cpu }} +{{- end }} diff --git a/gapps-chart/templates/gapps-ingress.yaml b/gapps-chart/templates/gapps-ingress.yaml new file mode 100644 index 0000000..a88c435 --- /dev/null +++ b/gapps-chart/templates/gapps-ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gapps-ingress + labels: + app.kubernetes.io/name: gapps + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + # Security Headers + nginx.ingress.kubernetes.io/proxy-body-size: 2500m + nginx.ingress.kubernetes.io/proxy-buffer-size: 12k + nginx.ingress.kubernetes.io/backend-protocol: HTTP + nginx.ingress.kubernetes.io/whitelist-source-range: {{ .Values.ingress.whitelistSourceRange }} + nginx.ingress.kubernetes.io/configuration-snippet: | + more_set_headers "X-Frame-Options SAMEORIGIN always"; + more_set_headers "X-XSS-Protection 1; mode=block"; + more_set_headers "Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload'"; + more_set_headers "X-Content-Type-Options nosniff"; + more_set_headers "Referrer-Policy strict-origin"; + more_set_headers "Feature-Policy geolocation 'none';midi 'none';sync-xhr 'none';microphone 'none';camera 'none';magnetometer 'none';gyroscope 'none';fullscreen 'self';payment 'none';"; + more_set_headers "Permissions-Policy geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"; + more_set_headers "server_tokens off;"; +spec: + ingressClassName: nginx + rules: + - host: {{ .Values.ingress.host }} # Use the host value from values.yaml + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Values.ingress.backendServiceName }} + port: + number: {{ .Values.ingress.backendServicePort }} + tls: + - hosts: + - {{ .Values.ingress.host }} + secretName: {{ .Values.ingress.tlsSecretName }} +{{- end }} \ No newline at end of file diff --git a/gapps-chart/templates/gapps-network-policy.yaml b/gapps-chart/templates/gapps-network-policy.yaml new file mode 100644 index 0000000..79ac699 --- /dev/null +++ b/gapps-chart/templates/gapps-network-policy.yaml @@ -0,0 +1,34 @@ +{{- if .Values.gapps.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: gapps-network-policy +spec: + podSelector: + matchLabels: + app: gapps + policyTypes: + - Ingress + - Egress + ingress: + - from: [] # Allows all incoming traffic, adjust as necessary + egress: + - to: + - podSelector: + matchLabels: + app: postgres + ports: + - protocol: TCP + port: 5432 + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 # Port for DNS + - to: [] # Allows all outbound SMTP traffic + ports: + - protocol: TCP + port: 587 # SMTP port (replace with your email server's port) +{{- end }} diff --git a/gapps-chart/templates/gapps-secrets.yaml b/gapps-chart/templates/gapps-secrets.yaml new file mode 100644 index 0000000..3624703 --- /dev/null +++ b/gapps-chart/templates/gapps-secrets.yaml @@ -0,0 +1,15 @@ +{{- if .Values.gappsSecrets.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: gapps-secrets +type: Opaque +data: + {{- with .Values.gappsSecrets }} + {{- range $key, $value := . }} + {{- if ne $key "enabled" }} + {{ $key }}: {{ $value | b64enc | quote }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/gapps-chart/templates/gapps-service.yaml b/gapps-chart/templates/gapps-service.yaml new file mode 100644 index 0000000..5db6796 --- /dev/null +++ b/gapps-chart/templates/gapps-service.yaml @@ -0,0 +1,14 @@ +{{- if .Values.gapps.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: gapps-service +spec: + selector: + app: gapps + ports: + - protocol: TCP + port: {{ .Values.gapps.service.port }} + targetPort: {{ .Values.gapps.service.targetPort }} + type: {{ .Values.gapps.service.type }} +{{- end }} diff --git a/gapps-chart/templates/gapps-worker-configmap.yaml b/gapps-chart/templates/gapps-worker-configmap.yaml new file mode 100644 index 0000000..6931fad --- /dev/null +++ b/gapps-chart/templates/gapps-worker-configmap.yaml @@ -0,0 +1,9 @@ +{{- if .Values.gappsWorker.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: gapps-worker-config +data: + WORKER_CONCURRENCY: "{{ .Values.gappsWorker.config.WORKER_CONCURRENCY }}" + WORKER_LOG_LEVEL: "{{ .Values.gappsWorker.config.WORKER_LOG_LEVEL }}" +{{- end }} diff --git a/gapps-chart/templates/gapps-worker-deployment.yaml b/gapps-chart/templates/gapps-worker-deployment.yaml new file mode 100644 index 0000000..1094418 --- /dev/null +++ b/gapps-chart/templates/gapps-worker-deployment.yaml @@ -0,0 +1,36 @@ +{{- if .Values.gappsWorker.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gapps-worker +spec: + replicas: {{ .Values.gappsWorker.replicaCount }} + selector: + matchLabels: + app: gapps-worker + template: + metadata: + labels: + app: gapps-worker + spec: + imagePullSecrets: + - name: {{ index .Values.commonImage.imagePullSecrets 0 "name" }} + containers: + - name: gapps-worker + image: {{ .Values.commonImage.repository }}:{{ .Values.commonImage.tag }} + envFrom: + - secretRef: + name: gapps-secrets + - configMapRef: + name: gapps-worker-config + env: + - name: AS_WORKER + value: {{ .Values.gappsWorker.environment.AS_WORKER | quote }} + resources: + requests: + memory: {{ .Values.gappsWorker.resources.requests.memory }} + cpu: {{ .Values.gappsWorker.resources.requests.cpu }} + limits: + memory: {{ .Values.gappsWorker.resources.limits.memory }} + cpu: {{ .Values.gappsWorker.resources.limits.cpu }} +{{- end }} diff --git a/gapps-chart/templates/gapps-worker-network-policy.yaml b/gapps-chart/templates/gapps-worker-network-policy.yaml new file mode 100644 index 0000000..16d0457 --- /dev/null +++ b/gapps-chart/templates/gapps-worker-network-policy.yaml @@ -0,0 +1,30 @@ +{{- if .Values.gappsWorker.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: gapps-worker-network-policy +spec: + podSelector: + matchLabels: + app: gapps-worker + policyTypes: + - Ingress + - Egress + ingress: + - from: [] # Add specific rules or leave empty to allow all incoming traffic + egress: + - to: + - podSelector: + matchLabels: + app: postgres + ports: + - protocol: TCP + port: 5432 + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 # Port for DNS +{{- end }} diff --git a/gapps-chart/templates/postgres-configmap.yaml b/gapps-chart/templates/postgres-configmap.yaml new file mode 100644 index 0000000..4ff0852 --- /dev/null +++ b/gapps-chart/templates/postgres-configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-init-scripts +data: + init-db.sh: | + #!/bin/bash + set -e + + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + SELECT 'CREATE DATABASE gapps' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'gapps')\\gexec + GRANT ALL PRIVILEGES ON DATABASE gapps TO $POSTGRES_USER; + EOSQL \ No newline at end of file diff --git a/gapps-chart/templates/postgres-secrets.yaml b/gapps-chart/templates/postgres-secrets.yaml new file mode 100644 index 0000000..042d54a --- /dev/null +++ b/gapps-chart/templates/postgres-secrets.yaml @@ -0,0 +1,10 @@ +{{- if .Values.postgres.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: postgres-credentials +type: Opaque +data: + username: {{ .Values.gappsSecrets.POSTGRES_USER | b64enc | quote }} + password: {{ .Values.gappsSecrets.POSTGRES_PASSWORD | b64enc | quote }} +{{- end }} diff --git a/gapps-chart/templates/postgres-service.yaml b/gapps-chart/templates/postgres-service.yaml new file mode 100644 index 0000000..31cdedd --- /dev/null +++ b/gapps-chart/templates/postgres-service.yaml @@ -0,0 +1,13 @@ +{{- if .Values.postgres.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: postgres-service +spec: + selector: + app: postgres + ports: + - protocol: TCP + port: {{ .Values.postgres.service.port }} + targetPort: {{ .Values.postgres.service.targetPort }} +{{- end }} diff --git a/gapps-chart/templates/postgres-statefulset.yaml b/gapps-chart/templates/postgres-statefulset.yaml new file mode 100644 index 0000000..34b340b --- /dev/null +++ b/gapps-chart/templates/postgres-statefulset.yaml @@ -0,0 +1,61 @@ +{{- if .Values.postgres.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres +spec: + serviceName: "postgres" + replicas: {{ .Values.postgres.replicaCount }} + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: {{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }} + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-credentials + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-credentials + key: password + - name: POSTGRES_DB + value: {{ .Values.postgres.dbName | quote }} + - name: PGDATA + value: {{ .Values.postgres.pgData | quote }} + resources: + requests: + memory: {{ .Values.postgres.resources.requests.memory }} + cpu: {{ .Values.postgres.resources.requests.cpu }} + limits: + memory: {{ .Values.postgres.resources.limits.memory }} + cpu: {{ .Values.postgres.resources.limits.cpu }} + volumeMounts: + - name: postgres-storage + mountPath: /data/postgres + - name: init-scripts + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init-scripts + configMap: + name: postgres-init-scripts + volumeClaimTemplates: + - metadata: + name: postgres-storage + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.postgres.persistence.size }} +{{- end }} diff --git a/gapps-chart/values.yaml b/gapps-chart/values.yaml new file mode 100644 index 0000000..02c594b --- /dev/null +++ b/gapps-chart/values.yaml @@ -0,0 +1,125 @@ +# Default values for gapps-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +showPostInstallNotes: true + +# Global values +replicaCount: 1 +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# Common image configuration for gapps and gapps-worker +commonImage: + repository: bmarsh13/gapps + tag: "3.5.3" + pullPolicy: IfNotPresent + imagePullSecrets: + - name: "dockerhub-secret" + + +# Ingress settings +ingress: + enabled: false + backendServiceName: gapps-service + backendServicePort: 5000 + tlsSecretName: example-tls + host: gapps.example.com # Specify the desired host here as a string + + # IP Whitelisting + # Configured to allow traffic only from Cloudflare IP ranges for enhanced security. + # This setting acts as an additional layer of security, restricting access to the specified IP ranges. + whitelistSourceRange: "173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13, 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22" + +# gapps application values +gapps: + enabled: true + resources: + requests: + memory: "128Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1" + service: + type: ClusterIP + port: 5000 + targetPort: 5000 + config: + DOC_LINK: "https://github.com/bmarsh9/gapps" + VERSION: "3.5.3" + APP_NAME: "Gapps" + ENABLE_SELF_REGISTRATION: "False" + RESET_DB: "no" + GUNICORN_WORKERS: "2" + MAIL_USE_TLS: "True" # Enables TLS encryption for secure email transmission. Set to "False" if using SSL encryption on port 465. + MAIL_USE_SSL: "False" # Enables SSL encryption for secure email transmission. Set to "True" if using SSL encryption on port 465. + MAIL_DEBUG: "True" # Enables debug information for the mail service. Useful for troubleshooting. Set to "False" in production for normal operation. + MAIL_DEFAULT_SENDER: "noreply@example.com" # The default email address used as the sender for outgoing emails. + MAIL_MAX_EMAILS: "10" # Limits the maximum number of emails to send per send() call. "None" means no limit. + MAIL_SUPPRESS_SEND: "False" # Suppresses actual sending of emails when set to "True". Useful for testing. Set to "True" in testing environments and "False" in production. + MAIL_ASCII_ATTACHMENTS: "False" # If set to "True", it ensures attachment filenames are ASCII only. + MAIL_SERVER: "localhost" # The SMTP server through which emails are sent. + MAIL_PORT: "587" # The port used for the SMTP server. Commonly 587 for TLS or 465 for SSL. + ONESHOT: "no" + INIT_MIGRATE: "no" + MIGRATE: "no" + configMapName: gapps-config + secretName: gapps-secrets + networkPolicy: + enabled: true + +# gapps-worker application values +gappsWorker: + enabled: true + resources: + requests: + memory: "128Mi" + cpu: "250m" + limits: + memory: "256Mi" + cpu: "500m" + config: + WORKER_CONCURRENCY: "1" + WORKER_LOG_LEVEL: "DEBUG" + environment: + AS_WORKER: "yes" + networkPolicy: + enabled: true + +# PostgreSQL database values +postgres: + enabled: true + replicaCount: 1 + image: + repository: postgres + tag: "latest" + dbName: "gapps" + pgData: "/data/postgres/pgdata" + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" + persistence: + size: "10Gi" + service: + port: 5432 + targetPort: 5432 + +# gapps secrets configuration +gappsSecrets: + enabled: true + # Uncomment and provide values for the secrets you want to set + POSTGRES_USER: "PLACEHOLDER" + POSTGRES_PASSWORD: "PLACEHOLDER" + POSTGRES_HOST: "PLACEHOLDER" + POSTGRES_DB: "PLACEHOLDER" + DEFAULT_EMAIL: "PLACEHOLDER" + DEFAULT_PASSWORD: "PLACEHOLDER" + SQLALCHEMY_DATABASE_URI: "PLACEHOLDER" + MAIL_USERNAME: "PLACEHOLDER" + MAIL_PASSWORD: "PLACEHOLDER" \ No newline at end of file