diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..3d68271 --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,121 @@ +steps: + build-frontend: + name: Build & Push Frontend + image: woodpeckerci/plugin-kaniko + when: + branch: [ master, develop ] + event: [ push, pull_request, tag ] + path: + include: [ frontend/** ] + settings: + registry: harbor.dvirlabs.com + repo: my-apps/${CI_REPO_NAME}-frontend + dockerfile: frontend/Dockerfile + context: frontend + tags: + - latest + - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}} + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + + build-backend: + name: Build & Push Backend + image: woodpeckerci/plugin-kaniko + when: + branch: [ master, develop ] + event: [ push, pull_request, tag ] + path: + include: [ backend/** ] + settings: + registry: harbor-core.dev-tools.svc.cluster.local + repo: my-apps/${CI_REPO_NAME}-backend + dockerfile: backend/Dockerfile + context: backend + tags: + - latest + - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}} + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + + update-values-frontend: + name: Update frontend tag in values.yaml + image: alpine:3.19 + when: + branch: [ master, develop ] + event: [ push ] + path: + include: [ frontend/** ] + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GIT_TOKEN: + from_secret: GIT_TOKEN + commands: + - apk add --no-cache git yq + - git config --global user.name "woodpecker-bot" + - git config --global user.email "ci@dvirlabs.com" + - git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git" + - cd my-apps + - | + TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" + echo "💡 Setting frontend tag to: $TAG" + yq -i ".frontend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + git add manifests/${CI_REPO_NAME}/values.yaml + git commit -m "frontend: update tag to $TAG" || echo "No changes" + git push origin HEAD + + update-values-backend: + name: Update backend tag in values.yaml + image: alpine:3.19 + when: + branch: [ master, develop ] + event: [ push ] + path: + include: [ backend/** ] + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GIT_TOKEN: + from_secret: GIT_TOKEN + commands: + - apk add --no-cache git yq + - git config --global user.name "woodpecker-bot" + - git config --global user.email "ci@dvirlabs.com" + - | + if [ ! -d "my-apps" ]; then + git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git" + fi + - cd my-apps + - | + TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" + echo "💡 Setting backend tag to: $TAG" + yq -i ".backend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + git add manifests/${CI_REPO_NAME}/values.yaml + git commit -m "backend: update tag to $TAG" || echo "No changes" + git push origin HEAD + + + trigger-gitops-via-push: + when: + branch: [ master, develop ] + event: [ push ] + name: Trigger apps-gitops via Git push + image: alpine/git + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GIT_TOKEN: + from_secret: GIT_TOKEN + commands: | + git config --global user.name "woodpecker-bot" + git config --global user.email "ci@dvirlabs.com" + git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/apps-gitops.git" + cd apps-gitops + echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger + git add .trigger + git commit -m "ci: trigger apps-gitops build" || echo "no changes" + git push origin HEAD \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..d8ce325 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,26 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.env +.venv +venv/ +ENV/ +env/ +.pytest_cache +.coverage +htmlcov/ +.tox/ +.mypy_cache/ +.dmypy.json +dmypy.json +*.log +.DS_Store +uploads/ +migrations/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..56a6192 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,32 @@ +# Use Python 3.11 slim image as base +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements file +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create uploads directory for product images +RUN mkdir -p /app/uploads/products + +# Expose port 8000 +EXPOSE 8000 + +# Set environment variables +ENV PYTHONUNBUFFERED=1 + +# Run the application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/routers/__pycache__/products.cpython-314.pyc b/backend/app/routers/__pycache__/products.cpython-314.pyc index 6fcd827..09e390c 100644 Binary files a/backend/app/routers/__pycache__/products.cpython-314.pyc and b/backend/app/routers/__pycache__/products.cpython-314.pyc differ diff --git a/backend/app/services/__pycache__/product.cpython-314.pyc b/backend/app/services/__pycache__/product.cpython-314.pyc index 2b29653..b593364 100644 Binary files a/backend/app/services/__pycache__/product.cpython-314.pyc and b/backend/app/services/__pycache__/product.cpython-314.pyc differ diff --git a/brand-master-chart/Chart.yaml b/brand-master-chart/Chart.yaml new file mode 100644 index 0000000..ee30f38 --- /dev/null +++ b/brand-master-chart/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: brand-master +description: A Helm chart for Brand Master - E-commerce Fashion & Shoe Store +type: application +version: 1.0.0 +appVersion: "1.0.0" +keywords: + - brand-master + - ecommerce + - fashion + - shoes +maintainers: + - name: dvir diff --git a/brand-master-chart/README.md b/brand-master-chart/README.md new file mode 100644 index 0000000..dca234e --- /dev/null +++ b/brand-master-chart/README.md @@ -0,0 +1,158 @@ +# Brand Master Helm Chart + +This Helm chart deploys the Brand Master e-commerce application on Kubernetes. + +## Components + +- **Frontend**: React-based UI served by Nginx +- **Backend**: FastAPI application +- **Database**: PostgreSQL 16 +- **Storage**: 15GB PVC for product images + +## Prerequisites + +- Kubernetes 1.19+ +- Helm 3.0+ +- cert-manager (for TLS certificates) +- Storage class configured (nfs-client by default) + +## Installation + +### 1. Update values.yaml + +Edit `values.yaml` and configure: + +- Image repositories (if using private registry) +- Domain names for ingress +- JWT secret key (IMPORTANT!) +- Database credentials +- Storage class name + +### 2. Install the chart + +```bash +# Install in the my-apps namespace +helm install brand-master ./brand-master-chart -n my-apps --create-namespace + +# Or with custom values +helm install brand-master ./brand-master-chart -n my-apps \ + --set backend.jwtSecretKey=your-super-secret-key \ + --set postgres.password=secure-password +``` + +### 3. Upgrade the chart + +```bash +helm upgrade brand-master ./brand-master-chart -n my-apps +``` + +### 4. Uninstall + +```bash +helm uninstall brand-master -n my-apps +``` + +## Configuration + +### Key Configuration Options + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `backend.image.repository` | Backend Docker image | `harbor.dvirlabs.com/my-apps/brand-master-backend` | +| `backend.image.tag` | Backend image tag | `latest` | +| `backend.jwtSecretKey` | JWT secret for authentication | `your-secret-key-change-this-in-production` | +| `backend.persistence.enabled` | Enable persistent storage for images | `true` | +| `backend.persistence.size` | Size of uploads PVC | `15Gi` | +| `frontend.image.repository` | Frontend Docker image | `harbor.dvirlabs.com/my-apps/brand-master-frontend` | +| `frontend.image.tag` | Frontend image tag | `latest` | +| `postgres.user` | PostgreSQL username | `brand_master_user` | +| `postgres.password` | PostgreSQL password | `brand_master_password` | +| `postgres.database` | PostgreSQL database name | `brand_master_db` | +| `postgres.persistence.size` | Size of database PVC | `10Gi` | + +## Building Docker Images + +### Backend + +```bash +cd backend +docker build -t harbor.dvirlabs.com/my-apps/brand-master-backend:latest . +docker push harbor.dvirlabs.com/my-apps/brand-master-backend:latest +``` + +### Frontend + +```bash +cd frontend +docker build -t harbor.dvirlabs.com/my-apps/brand-master-frontend:latest \ + --build-arg VITE_API_URL=https://api-brand-master.dvirlabs.com . +docker push harbor.dvirlabs.com/my-apps/brand-master-frontend:latest +``` + +## Storage + +The chart creates two PVCs: + +1. **Database PVC**: 10GB for PostgreSQL data +2. **Uploads PVC**: 15GB for product images at `/app/uploads` + +Both use the `nfs-client` storage class by default. Update this in `values.yaml` if needed. + +## Ingress + +The chart creates two ingress resources: + +- **Frontend**: `brand-master.dvirlabs.com` +- **Backend API**: `api-brand-master.dvirlabs.com` + +TLS is enabled by default using Let's Encrypt via cert-manager. + +## Troubleshooting + +### Check pod status +```bash +kubectl get pods -n my-apps +``` + +### View logs +```bash +# Backend logs +kubectl logs -n my-apps -l app.kubernetes.io/component=backend + +# Frontend logs +kubectl logs -n my-apps -l app.kubernetes.io/component=frontend + +# Database logs +kubectl logs -n my-apps -l app.kubernetes.io/component=database +``` + +### Access services locally +```bash +# Frontend +kubectl port-forward -n my-apps svc/brand-master-frontend 8080:80 + +# Backend +kubectl port-forward -n my-apps svc/brand-master-backend 8000:8000 + +# Database +kubectl port-forward -n my-apps svc/brand-master-db 5432:5432 +``` + +### Check PVC status +```bash +kubectl get pvc -n my-apps +``` + +## Security Notes + +1. **Change the JWT secret** in production +2. **Update database credentials** +3. **Use strong passwords** +4. **Configure proper CORS settings** +5. **Review and adjust resource limits** +6. **Enable network policies** if needed +7. **Use image pull secrets** for private registries + +## Support + +For issues or questions, refer to the main repository documentation. diff --git a/brand-master-chart/templates/NOTES.txt b/brand-master-chart/templates/NOTES.txt new file mode 100644 index 0000000..bb083f4 --- /dev/null +++ b/brand-master-chart/templates/NOTES.txt @@ -0,0 +1,48 @@ +1. Get the application URL by running these commands: +{{- if .Values.frontend.ingress.enabled }} +{{- range $host := .Values.frontend.ingress.hosts }} + {{- range .paths }} + Frontend: http{{ if $.Values.frontend.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- end }} + +{{- if .Values.backend.ingress.enabled }} +{{- range $host := .Values.backend.ingress.hosts }} + {{- range .paths }} + Backend API: http{{ if $.Values.backend.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- end }} + +2. Database Connection: + Host: {{ include "brand-master.fullname" . }}-db + Port: {{ .Values.postgres.port }} + Database: {{ .Values.postgres.database }} + User: {{ .Values.postgres.user }} + +3. Product Images Storage: + {{- if .Values.backend.persistence.enabled }} + PVC: {{ include "brand-master.fullname" . }}-uploads-pvc + Size: {{ .Values.backend.persistence.size }} + Mount Path: {{ .Values.backend.persistence.mountPath }} + {{- else }} + Warning: Persistence is disabled. Product images will be lost on pod restart! + {{- end }} + +4. IMPORTANT Security Notes: + - Change the JWT secret key in values.yaml before deploying to production + - Update the database password in values.yaml + - Configure your domain names in the ingress sections + - Ensure cert-manager is installed for TLS certificates + +5. To access the application locally without ingress: + kubectl port-forward svc/{{ include "brand-master.fullname" . }}-frontend 8080:80 + kubectl port-forward svc/{{ include "brand-master.fullname" . }}-backend 8000:8000 + +6. To check pod status: + kubectl get pods -l app.kubernetes.io/instance={{ .Release.Name }} + +7. To view logs: + kubectl logs -l app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=backend + kubectl logs -l app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=frontend diff --git a/brand-master-chart/templates/_helpers.tpl b/brand-master-chart/templates/_helpers.tpl new file mode 100644 index 0000000..09c6169 --- /dev/null +++ b/brand-master-chart/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "brand-master.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "brand-master.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "brand-master.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "brand-master.labels" -}} +helm.sh/chart: {{ include "brand-master.chart" . }} +{{ include "brand-master.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "brand-master.selectorLabels" -}} +app.kubernetes.io/name: {{ include "brand-master.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "brand-master.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "brand-master.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/brand-master-chart/templates/backend-deployment.yaml b/brand-master-chart/templates/backend-deployment.yaml new file mode 100644 index 0000000..de46554 --- /dev/null +++ b/brand-master-chart/templates/backend-deployment.yaml @@ -0,0 +1,106 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "brand-master.fullname" . }}-backend + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + {{- include "brand-master.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: backend + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "brand-master.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: backend + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "brand-master.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: wait-for-postgres + image: busybox:1.35 + command: ['sh', '-c', 'until nc -z {{ include "brand-master.fullname" . }}-db-headless {{ .Values.postgres.port | default 5432 }}; do echo waiting for postgres; sleep 2; done;'] + containers: + - name: backend + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.backend.service.targetPort }} + protocol: TCP + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: database-url + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: jwt-secret-key + - name: ALGORITHM + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: jwt-algorithm + - name: ACCESS_TOKEN_EXPIRE_MINUTES + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: jwt-expire-minutes + {{- range $key, $value := .Values.backend.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + volumeMounts: + {{- if .Values.backend.persistence.enabled }} + - name: uploads + mountPath: {{ .Values.backend.persistence.mountPath }} + {{- end }} + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + volumes: + {{- if .Values.backend.persistence.enabled }} + - name: uploads + persistentVolumeClaim: + claimName: {{ include "brand-master.fullname" . }}-uploads-pvc + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/brand-master-chart/templates/backend-ingress.yaml b/brand-master-chart/templates/backend-ingress.yaml new file mode 100644 index 0000000..9e37256 --- /dev/null +++ b/brand-master-chart/templates/backend-ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.backend.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "brand-master.fullname" . }}-backend + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: backend + {{- with .Values.backend.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.backend.ingress.className }} + ingressClassName: {{ .Values.backend.ingress.className }} + {{- end }} + {{- if .Values.backend.ingress.tls }} + tls: + {{- range .Values.backend.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.backend.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "brand-master.fullname" $ }}-backend + port: + number: {{ $.Values.backend.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/brand-master-chart/templates/backend-pvc.yaml b/brand-master-chart/templates/backend-pvc.yaml new file mode 100644 index 0000000..fa8eb3f --- /dev/null +++ b/brand-master-chart/templates/backend-pvc.yaml @@ -0,0 +1,18 @@ +{{- if .Values.backend.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "brand-master.fullname" . }}-uploads-pvc + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + accessModes: + - {{ .Values.backend.persistence.accessMode }} + {{- if .Values.backend.persistence.storageClass }} + storageClassName: {{ .Values.backend.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.backend.persistence.size }} +{{- end }} diff --git a/brand-master-chart/templates/backend-service.yaml b/brand-master-chart/templates/backend-service.yaml new file mode 100644 index 0000000..04a75ec --- /dev/null +++ b/brand-master-chart/templates/backend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "brand-master.fullname" . }}-backend + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + type: {{ .Values.backend.service.type }} + ports: + - port: {{ .Values.backend.service.port }} + targetPort: {{ .Values.backend.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "brand-master.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: backend diff --git a/brand-master-chart/templates/db-service.yaml b/brand-master-chart/templates/db-service.yaml new file mode 100644 index 0000000..d3ff669 --- /dev/null +++ b/brand-master-chart/templates/db-service.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "brand-master.fullname" . }}-db + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + type: {{ .Values.postgres.service.type }} + ports: + - port: {{ .Values.postgres.service.port }} + targetPort: {{ .Values.postgres.service.targetPort }} + protocol: TCP + name: postgres + selector: + {{- include "brand-master.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: database +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "brand-master.fullname" . }}-db-headless + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + type: ClusterIP + clusterIP: None + ports: + - port: {{ .Values.postgres.service.port }} + targetPort: {{ .Values.postgres.service.targetPort }} + protocol: TCP + name: postgres + selector: + {{- include "brand-master.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: database diff --git a/brand-master-chart/templates/db-statefulset.yaml b/brand-master-chart/templates/db-statefulset.yaml new file mode 100644 index 0000000..5debdc0 --- /dev/null +++ b/brand-master-chart/templates/db-statefulset.yaml @@ -0,0 +1,124 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "brand-master.fullname" . }}-db + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + serviceName: {{ include "brand-master.fullname" . }}-db-headless + replicas: 1 + selector: + matchLabels: + {{- include "brand-master.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: database + template: + metadata: + labels: + {{- include "brand-master.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: database + spec: + securityContext: + fsGroup: 999 + initContainers: + - name: fix-permissions + image: busybox:latest + command: + - sh + - -c + - | + chown -R 999:999 /var/lib/postgresql/data || true + chmod 700 /var/lib/postgresql/data || true + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 0 + containers: + - name: postgres + securityContext: + runAsUser: 999 + runAsNonRoot: true + image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" + imagePullPolicy: {{ .Values.postgres.image.pullPolicy }} + ports: + - name: postgres + containerPort: {{ .Values.postgres.port }} + protocol: TCP + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: postgres-user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: postgres-password + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: {{ include "brand-master.fullname" . }}-secrets + key: postgres-database + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + - name: postgres-run + mountPath: /var/run/postgresql + resources: + {{- toYaml .Values.postgres.resources | nindent 12 }} + startupProbe: + exec: + command: + - sh + - -c + - pg_isready -h 127.0.0.1 -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB" + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 30 + livenessProbe: + exec: + command: + - sh + - -c + - pg_isready -h 127.0.0.1 -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - sh + - -c + - pg_isready -h 127.0.0.1 -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB" + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: postgres-run + emptyDir: {} + {{- if .Values.postgres.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: postgres-data + labels: + {{- include "brand-master.labels" . | nindent 8 }} + spec: + accessModes: + - {{ .Values.postgres.persistence.accessMode }} + {{- if .Values.postgres.persistence.storageClass }} + storageClassName: {{ .Values.postgres.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgres.persistence.size }} + {{- else }} + - name: postgres-data + emptyDir: {} + {{- end }} diff --git a/brand-master-chart/templates/frontend-deployment.yaml b/brand-master-chart/templates/frontend-deployment.yaml new file mode 100644 index 0000000..274a172 --- /dev/null +++ b/brand-master-chart/templates/frontend-deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "brand-master.fullname" . }}-frontend + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + replicas: {{ .Values.frontend.replicaCount }} + selector: + matchLabels: + {{- include "brand-master.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: frontend + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "brand-master.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: frontend + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "brand-master.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: frontend + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.frontend.service.targetPort }} + protocol: TCP + {{- if .Values.frontend.env }} + env: + {{- range $key, $value := .Values.frontend.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/brand-master-chart/templates/frontend-ingress.yaml b/brand-master-chart/templates/frontend-ingress.yaml new file mode 100644 index 0000000..d8d2de4 --- /dev/null +++ b/brand-master-chart/templates/frontend-ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.frontend.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "brand-master.fullname" . }}-frontend + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend + {{- with .Values.frontend.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.frontend.ingress.className }} + ingressClassName: {{ .Values.frontend.ingress.className }} + {{- end }} + {{- if .Values.frontend.ingress.tls }} + tls: + {{- range .Values.frontend.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.frontend.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "brand-master.fullname" $ }}-frontend + port: + number: {{ $.Values.frontend.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/brand-master-chart/templates/frontend-service.yaml b/brand-master-chart/templates/frontend-service.yaml new file mode 100644 index 0000000..a2326c7 --- /dev/null +++ b/brand-master-chart/templates/frontend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "brand-master.fullname" . }}-frontend + labels: + {{- include "brand-master.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + type: {{ .Values.frontend.service.type }} + ports: + - port: {{ .Values.frontend.service.port }} + targetPort: {{ .Values.frontend.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "brand-master.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: frontend diff --git a/brand-master-chart/templates/secret.yaml b/brand-master-chart/templates/secret.yaml new file mode 100644 index 0000000..e69908a --- /dev/null +++ b/brand-master-chart/templates/secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "brand-master.fullname" . }}-secrets + labels: + {{- include "brand-master.labels" . | nindent 4 }} +type: Opaque +stringData: + postgres-user: {{ .Values.postgres.user | quote }} + postgres-password: {{ .Values.postgres.password | quote }} + postgres-database: {{ .Values.postgres.database | quote }} + database-url: "postgresql://{{ .Values.postgres.user }}:{{ .Values.postgres.password }}@{{ include "brand-master.fullname" . }}-db:{{ .Values.postgres.port }}/{{ .Values.postgres.database }}" + jwt-secret-key: {{ .Values.backend.jwtSecretKey | quote }} + jwt-algorithm: {{ .Values.backend.jwtAlgorithm | quote }} + jwt-expire-minutes: {{ .Values.backend.jwtExpireMinutes | quote }} diff --git a/brand-master-chart/templates/serviceaccount.yaml b/brand-master-chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..de9c270 --- /dev/null +++ b/brand-master-chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "brand-master.serviceAccountName" . }} + labels: + {{- include "brand-master.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/brand-master-chart/values.yaml b/brand-master-chart/values.yaml new file mode 100644 index 0000000..9286702 --- /dev/null +++ b/brand-master-chart/values.yaml @@ -0,0 +1,165 @@ +global: + namespace: my-apps + imagePullSecrets: [] + +# Backend configuration +backend: + name: backend + replicaCount: 1 + image: + repository: harbor.dvirlabs.com/my-apps/brand-master-backend + pullPolicy: IfNotPresent + tag: "latest" + + service: + type: ClusterIP + port: 8000 + targetPort: 8000 + + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + + env: + PYTHONUNBUFFERED: "1" + BACKEND_URL: "https://api-brand-master.dvirlabs.com" + FRONTEND_URL: "https://brand-master.dvirlabs.com" + + # JWT Secret Key (IMPORTANT: Change this in production!) + jwtSecretKey: "your-secret-key-change-this-in-production" + jwtAlgorithm: "HS256" + jwtExpireMinutes: "30" + + # Persistent storage for product images + persistence: + enabled: true + storageClass: "nfs-client" + accessMode: ReadWriteOnce + size: 15Gi + mountPath: /app/uploads + + ingress: + enabled: true + className: "traefik" + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hosts: + - host: api-brand-master.dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: api-brand-master-tls + hosts: + - api-brand-master.dvirlabs.com + +# Frontend configuration +frontend: + name: frontend + replicaCount: 1 + image: + repository: harbor.dvirlabs.com/my-apps/brand-master-frontend + pullPolicy: IfNotPresent + tag: "latest" + + service: + type: ClusterIP + port: 80 + targetPort: 80 + + env: + VITE_API_URL: "https://api-brand-master.dvirlabs.com" + + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + + ingress: + enabled: true + className: "traefik" + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hosts: + - host: brand-master.dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: brand-master-tls + hosts: + - brand-master.dvirlabs.com + +# PostgreSQL configuration +postgres: + name: db + image: + repository: postgres + tag: "16-alpine" + pullPolicy: IfNotPresent + + user: brand_master_user + password: brand_master_password + database: brand_master_db + port: 5432 + + service: + type: ClusterIP + port: 5432 + targetPort: 5432 + + persistence: + enabled: true + accessMode: ReadWriteOnce + storageClass: "nfs-client" + size: 10Gi + + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + +# Service Account +serviceAccount: + create: true + annotations: {} + name: "" + +# Pod annotations +podAnnotations: {} + +# Pod security context +podSecurityContext: {} + # fsGroup: 2000 + +# Container security context +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# Node selector +nodeSelector: {} + +# Tolerations +tolerations: [] + +# Affinity +affinity: {} diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..a830978 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,19 @@ +node_modules +dist +.git +.gitignore +README.md +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +*.swp +*.swo +*~ +.vscode +.idea diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..53518d8 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,60 @@ +# Build stage +FROM node:18-alpine AS build + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json* ./ + +# Install dependencies +RUN npm install + +# Copy application code +COPY . . + +# Build argument for API URL (can be overridden at build time) +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Remove default nginx config +RUN rm /etc/nginx/conf.d/default.conf + +# Create custom nginx config +RUN echo 'server {' > /etc/nginx/conf.d/default.conf && \ + echo ' listen 80;' >> /etc/nginx/conf.d/default.conf && \ + echo ' server_name _;' >> /etc/nginx/conf.d/default.conf && \ + echo ' root /usr/share/nginx/html;' >> /etc/nginx/conf.d/default.conf && \ + echo ' index index.html;' >> /etc/nginx/conf.d/default.conf && \ + echo '' >> /etc/nginx/conf.d/default.conf && \ + echo ' # Enable gzip compression' >> /etc/nginx/conf.d/default.conf && \ + echo ' gzip on;' >> /etc/nginx/conf.d/default.conf && \ + echo ' gzip_vary on;' >> /etc/nginx/conf.d/default.conf && \ + echo ' gzip_min_length 1024;' >> /etc/nginx/conf.d/default.conf && \ + echo ' gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;' >> /etc/nginx/conf.d/default.conf && \ + echo '' >> /etc/nginx/conf.d/default.conf && \ + echo ' location / {' >> /etc/nginx/conf.d/default.conf && \ + echo ' try_files $uri $uri/ /index.html;' >> /etc/nginx/conf.d/default.conf && \ + echo ' }' >> /etc/nginx/conf.d/default.conf && \ + echo '' >> /etc/nginx/conf.d/default.conf && \ + echo ' # Cache static assets' >> /etc/nginx/conf.d/default.conf && \ + echo ' location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {' >> /etc/nginx/conf.d/default.conf && \ + echo ' expires 1y;' >> /etc/nginx/conf.d/default.conf && \ + echo ' add_header Cache-Control "public, immutable";' >> /etc/nginx/conf.d/default.conf && \ + echo ' }' >> /etc/nginx/conf.d/default.conf && \ + echo '}' >> /etc/nginx/conf.d/default.conf + +# Copy built files from build stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"]