Compare commits

..
50 Commits
Author SHA1 Message Date
Adrien Bouvais 8e8ebce721 Moved back test bucket in image
Build and Push Docker Images / build-and-push (push) Successful in 1m6s
2026-08-16 13:52:05 +02:00
Adrien Bouvais 756a781524 Update notebook image path 2026-08-16 13:50:04 +02:00
Adrien Bouvais 0ffe89a881 Moved bucket test py
Build and Push Docker Images / build-and-push (push) Successful in 1m13s
2026-08-16 13:41:20 +02:00
Adrien Bouvais 5287ca66a1 Fix CI tests
Build and Push Docker Images / build-and-push (push) Successful in 1m7s
2026-08-16 13:24:03 +02:00
Adrien Bouvais cb63016421 Added tests for images building
Build and Push Docker Images / build-and-push (push) Failing after 27s
2026-08-16 10:34:32 +02:00
Adrien Bouvais 43802259ed Redo builded action to use only shell 2026-08-16 10:33:09 +02:00
Adrien Bouvais c8046eb98f Added tests to CI
Build and Push Docker Images / build-and-push (push) Failing after 58s
2026-08-15 23:21:26 +02:00
Adrien Bouvais 57d1335fba Fix
Build and Push Docker Images / build-and-push (push) Failing after 34s
2026-08-15 20:14:54 +02:00
Adrien Bouvais ea7ee88ce1 Removed unknow AI stuff 2026-08-15 20:13:43 +02:00
Adrien Bouvais a20c7e2c59 Fix
Build and Push Docker Images / build-and-push (push) Failing after 1m15s
2026-08-15 20:12:15 +02:00
Adrien Bouvais 6945db40e1 With last commit 2026-08-15 20:10:22 +02:00
Adrien Bouvais 1d8cfbfc07 Renamed test_buckets 2026-08-15 20:10:08 +02:00
Adrien Bouvais 74da180a07 Removed label
Build and Push Docker Images / build-and-push (push) Successful in 1m49s
2026-08-15 18:03:21 +02:00
Adrien Bouvais 0aa9488627 Fixs
Build and Push Docker Images / build-and-push (push) Failing after 1m34s
2026-08-15 17:19:35 +02:00
Adrien Bouvais 2782d6d43d Add base build name in jupyter step to build it
Build and Push Docker Images / build-and-push (push) Successful in 4m32s
2026-08-15 16:55:47 +02:00
Adrien Bouvais 82cf6bf7e6 Only on tag push
Build and Push Docker Images / build-and-push (push) Failing after 1m36s
2026-08-15 16:49:27 +02:00
Adrien Bouvais 7662463c7d Removed CI condition
Build and Push Docker Images / build-and-push (push) Failing after 2m58s
2026-08-15 16:45:26 +02:00
Adrien Bouvais 9ee2fd2837 Fix pipeline cred 2026-08-15 16:41:19 +02:00
Adrien Bouvais 2fef740a5f Added tenant-b + removed name in config to use path
Build and Push Docker Images / build-and-push (push) Failing after 29s
2026-08-15 16:38:02 +02:00
Adrien Bouvais 7abb8db1e4 Removed comments
Build and Push Docker Images / build-and-push (push) Failing after 4m51s
2026-08-15 16:26:41 +02:00
Adrien Bouvais 189a3c4e0c Added images build CI 2026-08-15 16:23:57 +02:00
Adrien Bouvais a5b006f728 Better script test + renamed AWS env var to MINIO 2026-08-15 16:23:02 +02:00
Adrien Bouvais e7b8102733 Fix 2026-08-15 16:18:25 +02:00
Adrien Bouvais 216b77c2ea Add hello file in ref bucket 2026-08-15 16:02:56 +02:00
Adrien Bouvais bae53cc0a4 Project can create namespace 2026-08-15 11:50:51 +02:00
Adrien Bouvais 714db6f03d Fix 2026-08-15 11:06:11 +02:00
Adrien Bouvais dcf9877fd0 Added tenant to minio policy name 2026-08-15 11:04:07 +02:00
Adrien Bouvais 3d3655a95e Prevent error if IAM already set 2026-08-15 11:03:01 +02:00
Adrien Bouvais 70c70ce756 Added a test script to notebook image 2026-08-15 10:04:27 +02:00
Adrien Bouvais ee2f211774 Added delay to deploy, to lets the passeword generate 2026-08-15 09:55:25 +02:00
Adrien Bouvais 4b3bb98bd5 Made minio setup more robustre 2026-08-15 09:54:43 +02:00
Adrien Bouvais 7e82888149 Added missing var 2026-08-15 09:27:40 +02:00
Adrien Bouvais 0951d32975 Added secrets
- A main admin password
- A random password for each SA per tenant
2026-08-15 09:24:10 +02:00
Adrien Bouvais b4e22f6cee Removed comment 2026-08-15 09:22:51 +02:00
Adrien Bouvais 6d89419c37 Fix 2026-08-14 23:56:15 +02:00
Adrien Bouvais df6b9e64df Added service account to tenant template 2026-08-14 23:55:15 +02:00
Adrien Bouvais d523cfc8ed Add registry credentials to pull images jupyter 2026-08-14 23:42:25 +02:00
Adrien Bouvais 7514d810b9 . 2026-08-14 17:05:13 +02:00
Adrien Bouvais 012e69917c Added .Values for name 2026-08-14 17:03:16 +02:00
Adrien Bouvais 5050aca22b . 2026-08-14 16:59:21 +02:00
Adrien Bouvais e4922fc3be Appset path fix 2026-08-14 16:37:36 +02:00
Adrien Bouvais 403b4b564e Added tenant chart 2026-08-14 16:35:16 +02:00
Adrien Bouvais 9396e07120 Renamed 2026-08-14 16:30:27 +02:00
Adrien Bouvais 365ae90db8 Splited gitops into 2 folder to prevent root-app to be recursif on itself 2026-08-14 16:10:32 +02:00
Adrien Bouvais c0c97cccf4 Renamed values to config.yaml 2026-08-14 16:04:42 +02:00
Adrien Bouvais f7683be745 Removed unecessary config 2026-08-14 16:02:14 +02:00
Adrien Bouvais 86eb2002f0 Added mioni manifests 2026-08-14 16:01:50 +02:00
Adrien Bouvais d204047979 Add gitops 2026-08-14 16:01:42 +02:00
Adrien Bouvais 7acdc5e970 Added base config for tenant-a 2026-08-14 14:31:01 +02:00
Adrien Bouvais 54ca0e3062 Added images 2026-08-14 14:29:19 +02:00
19 changed files with 527 additions and 2 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Build and Push Docker Images
on:
push:
tags:
- '*.*.*'
env:
REGISTRY: registry.bouvais.lu
BASE_IMAGE_NAME: tenant/base
JUPYTER_IMAGE_NAME: tenant/jupyter
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
run: |
docker login ${{ env.REGISTRY }} \
--username adrien \
--password "${{ secrets.DOCKER_PASSWORD }}"
# --- 1. BUILD & TEST BASE IMAGE ---
- name: Build Base image
run: |
docker build \
-f ./images/base/dockerfile \
-t ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ github.ref_name }} \
./images/base
- name: Test Base image
run: |
cid=$(docker create -u root -w /tests ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ github.ref_name }} sleep 60)
docker start "$cid"
docker cp ${{ github.workspace }}/tests/. "$cid":/tests
docker exec "$cid" python3 /tests/base.py
docker rm -f "$cid"
- name: Tag Base image as latest
run: |
docker tag ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ github.ref_name }} \
${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest
- name: Push Base image
run: |
docker push ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ github.ref_name }}
docker push ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest
# --- 2. BUILD & TEST JUPYTER IMAGE ---
- name: Build Jupyter image
run: |
docker build \
-f ./images/jupyter/dockerfile \
--build-arg BASE_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ github.ref_name }} \
-t ${{ env.REGISTRY }}/${{ env.JUPYTER_IMAGE_NAME }}:${{ github.ref_name }} \
./images/jupyter
- name: Test Jupyter dependencies
run: |
cid=$(docker create -w /tests ${{ env.REGISTRY }}/${{ env.JUPYTER_IMAGE_NAME }}:${{ github.ref_name }} sleep 60)
docker start "$cid"
docker cp ${{ github.workspace }}/tests/. "$cid":/tests
docker exec "$cid" python3 /tests/dependencies.py
docker rm -f "$cid"
- name: Tag Jupyter image as latest
run: |
docker tag ${{ env.REGISTRY }}/${{ env.JUPYTER_IMAGE_NAME }}:${{ github.ref_name }} \
${{ env.REGISTRY }}/${{ env.JUPYTER_IMAGE_NAME }}:latest
- name: Push Jupyter image
run: |
docker push ${{ env.REGISTRY }}/${{ env.JUPYTER_IMAGE_NAME }}:${{ github.ref_name }}
docker push ${{ env.REGISTRY }}/${{ env.JUPYTER_IMAGE_NAME }}:latest
+45 -2
View File
@@ -13,12 +13,17 @@ brew install docker
# 2. Images
```
To created the needed dependency, I did a simple 2 images base + jupyter.
The first image is a minimal python slim. I then add some
### 2.1. Base
```images/base/dockerfile
FROM python:3.12-slim-bookworm AS base
LABEL org.opencontainers.image.title="tenant-base" \
org.opencontainers.image.description="Hardened base image for tenant workspaces" \
org.opencontainers.image.source="https://github.com/yourorg/yourrepo"
org.opencontainers.image.source="https://git.bouvais.lu/adrien/"
# System deps only — keep this layer stable so it's rarely rebuilt
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -36,3 +41,41 @@ USER appuser
ENTRYPOINT ["tini", "--"]
```
Then build and push it to the registry
```
docker build -t registry.bouvais.lu/tenant-base:1.0.0 images/base
docker push registry.bouvais.lu/tenant-base:1.0.0
```
### 2.2. Jupyter
Now I dp the same for a simple jupyter image.
```
ARG BASE_IMAGE=registry.bouvais.lu/tenant-base:1.0.0
FROM ${BASE_IMAGE}
LABEL org.opencontainers.image.title="tenant-jupyter" \
org.opencontainers.image.description="JupyterLab image built on tenant-base" \
org.opencontainers.image.base.name="${BASE_IMAGE}"
USER root
COPY --chown=appuser:appuser requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt \
&& rm /tmp/requirements.txt
# No secrets baked in — S3 creds come from a mounted K8s Secret / env at runtime
USER appuser
WORKDIR /home/appuser/work
EXPOSE 8888
CMD ["jupyter", "lab", \
"--ip=0.0.0.0", \
"--port=8888", \
"--no-browser", \
"--ServerApp.token=", \
"--ServerApp.allow_remote_access=True"]
```
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: tenant
description: Helm chart for workspace tenant
type: application
version: 0.1.0
appVersion: "1.0.0"
+39
View File
@@ -0,0 +1,39 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: jupyter
namespace: {{ .Release.Namespace }}
labels:
app: jupyter
tenant: 'tenant-{{.Values.name}}'
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
replicas: 1
selector:
matchLabels:
app: jupyter
tenant: 'tenant-{{.Values.name}}'
template:
metadata:
labels:
app: jupyter
tenant: 'tenant-{{.Values.name}}'
spec:
serviceAccountName: 'tenant-{{ .Values.name }}-sa'
imagePullSecrets:
- name: registry-credentials
containers:
- name: jupyter
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
envFrom:
- secretRef:
name: s3-credentials
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
+100
View File
@@ -0,0 +1,100 @@
apiVersion: batch/v1
kind: Job
metadata:
name: 'tenant-{{ .Values.name }}-minio-setup'
namespace: {{ .Release.Namespace }}
annotations:
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
ttlSecondsAfterFinished: 100
template:
spec:
restartPolicy: OnFailure
containers:
- name: mc
image: minio/mc:RELEASE.2024-03-03T00-13-08Z
env:
# 1. MinIO Admin Credentials (Replicated from argocd namespace)
- name: MINIO_ADMIN_USER
valueFrom:
secretKeyRef:
name: minio-admin-credentials
key: MINIO_ROOT_USER
- name: MINIO_ADMIN_PASS
valueFrom:
secretKeyRef:
name: minio-admin-credentials
key: MINIO_ROOT_PASSWORD
# 2. Randomly Generated Tenant Credentials
- name: TENANT_USER
valueFrom:
secretKeyRef:
name: s3-credentials
key: MINIO_ACCESS_KEY_ID
- name: TENANT_PASS
valueFrom:
secretKeyRef:
name: s3-credentials
key: MINIO_SECRET_ACCESS_KEY
- name: BUCKET_REF
valueFrom:
secretKeyRef:
name: s3-credentials
key: BUCKET_REF
- name: BUCKET_WORK
valueFrom:
secretKeyRef:
name: s3-credentials
key: BUCKET_WORK
command:
- /bin/sh
- -c
- |
# Fail immediately if any command fails
set -e
# Connect to MinIO using quotes to protect special characters in passwords
mc alias set minioadmin http://minio.minio.svc.cluster.local:9000 "$MINIO_ADMIN_USER" "$MINIO_ADMIN_PASS"
# Create buckets
mc mb --ignore-existing minioadmin/$BUCKET_REF
mc mb --ignore-existing minioadmin/$BUCKET_WORK
echo "hello tenant-{{ .Values.name }}" | mc pipe minioadmin/$BUCKET_REF/hello.txt
# Create tenant user (REMOVED || true)
mc admin user add minioadmin "$TENANT_USER" "$TENANT_PASS"
# Apply IAM isolation policy
cat <<EOF > /tmp/policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::$BUCKET_REF",
"arn:aws:s3:::$BUCKET_WORK"
]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::$BUCKET_REF/*"]
},
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::$BUCKET_WORK/*"]
}
]
}
EOF
mc admin policy create minioadmin tenant-{{ .Values.name }}-policy /tmp/policy.json || mc admin policy update minioadmin tenant-{{ .Values.name }}-policy /tmp/policy.json
# Good enough for now, would need better error handeling
mc admin policy attach minioadmin tenant-{{ .Values.name }}-policy --user="$TENANT_USER" || echo "Policy already attached, skipping."
echo "MinIO setup completed for tenant-{{ .Values.name }}"
+16
View File
@@ -0,0 +1,16 @@
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace "s3-credentials" -}}
apiVersion: v1
kind: Secret
metadata:
name: s3-credentials
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/tenant: 'tenant-{{ .Values.name }}'
type: Opaque
stringData:
MINIO_ACCESS_KEY_ID: 'tenant-{{ .Values.name }}-user'
# If the secret exist, keep. Otherwise create a new random one. (To prevent a new password at each sync).
MINIO_SECRET_ACCESS_KEY: {{ if and $existingSecret $existingSecret.data (hasKey $existingSecret.data "AWS_SECRET_ACCESS_KEY") }}{{ index $existingSecret.data "AWS_SECRET_ACCESS_KEY" | b64dec }}{{ else }}{{ randAlphaNum 24 }}{{ end }}
S3_ENDPOINT: "http://minio.minio.svc.cluster.local:9000"
BUCKET_REF: "ref-tenant-{{ .Values.name }}"
BUCKET_WORK: "work-tenant-{{ .Values.name }}"
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: 'tenant-{{ .Values.name }}-sa'
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: jupyter
app.kubernetes.io/tenant: 'tenant-{{ .Values.name }}'
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://git.bouvais.lu/adrien/ctie-exercice
targetRevision: main
path: gitops/tenants
directory:
recurse: true
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
+41
View File
@@ -0,0 +1,41 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: tenants
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: https://git.bouvais.lu/adrien/ctie-exercice
revision: main
files:
- path: "tenants/*/config.yaml"
template:
metadata:
name: 'tenant-{{ trimPrefix "tenant-" .path.basename }}'
spec:
project: tenants
sources:
- repoURL: https://git.bouvais.lu/adrien/ctie-exercice
targetRevision: main
path: charts/tenant
helm:
valueFiles:
- '$values/{{.path.path}}/config.yaml'
parameters:
- name: name
value: '{{ trimPrefix "tenant-" .path.basename }}'
- repoURL: https://git.bouvais.lu/adrien/ctie-exercice
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: 'tenant-{{ trimPrefix "tenant-" .path.basename }}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+19
View File
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: tenants
namespace: argocd
spec:
sourceRepos:
- https://git.bouvais.lu/adrien/ctie-exercice
destinations:
- server: https://kubernetes.default.svc
namespace: 'tenant-*'
clusterResourceWhitelist:
- group: ''
kind: Namespace
namespaceResourceWhitelist:
- group: '*'
kind: '*'
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.12-slim-bookworm AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
tini \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --gid 1000 appuser \
&& useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser
WORKDIR /home/appuser
USER appuser
ENTRYPOINT ["tini", "--"]
+20
View File
@@ -0,0 +1,20 @@
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
USER root
COPY --chown=appuser:appuser test_buckets.py /home/appuser/work/test_buckets.py
COPY --chown=appuser:appuser requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt \
&& rm /tmp/requirements.txt
USER appuser
WORKDIR /home/appuser/work
EXPOSE 8888
CMD ["jupyter", "lab", \
"--ip=0.0.0.0", \
"--port=8888", \
"--no-browser", \
"--ServerApp.token=", \
"--ServerApp.allow_remote_access=True"]
+4
View File
@@ -0,0 +1,4 @@
jupyterlab==4.2.5
boto3==1.35.0
s3fs==2024.9.0
pandas==2.2.2
+37
View File
@@ -0,0 +1,37 @@
import os
import boto3
s3 = boto3.client(
's3',
endpoint_url=os.environ['S3_ENDPOINT'],
aws_access_key_id=os.environ['MINIO_ACCESS_KEY_ID'],
aws_secret_access_key=os.environ['MINIO_SECRET_ACCESS_KEY']
)
ref_bucket = os.environ['BUCKET_REF']
work_bucket = os.environ['BUCKET_WORK']
# 1. READ FROM REFERENCE BUCKET (Should PASS)
response = s3.list_objects_v2(Bucket=ref_bucket)
data = s3.get_object(Bucket=ref_bucket, Key="hello.txt")['Body'].read()
print("SUCCESS: List items in ref bucket:", response['KeyCount'], data.decode())
# 2. WRITE TO REFERENCE BUCKET (Should FAIL with 403 Forbidden)
try:
s3.put_object(Bucket=ref_bucket, Key="test.txt", Body=b"forbidden write")
print("ERROR: Write succeeded when it should be forbidden!")
except Exception as e:
print("SUCCESS: Write to reference bucket blocked:", e)
# 3. WRITE & READ WORK BUCKET (Should PASS)
s3.put_object(Bucket=work_bucket, Key="data.txt", Body=b"hello tenant")
data = s3.get_object(Bucket=work_bucket, Key="data.txt")['Body'].read()
print("SUCCESS: Read back from work bucket:", data.decode())
# 4. ACCESS OTHER TENANT BUCKET (Should FAIL with Access Denied)
try:
s3.list_objects_v2(Bucket="work-tenant-b")
print("ERROR: Accessed tenant-b bucket!")
except Exception as e:
print("SUCCESS: Access to tenant-b blocked:", e)
+52
View File
@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
namespace: minio
spec:
replicas: 1
selector:
matchLabels: {app: minio}
template:
metadata:
labels: {app: minio}
spec:
containers:
- name: minio
image: minio/minio:latest
args: ["server", "/data", "--console-address", ":9001"]
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: minio-admin-credentials
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: minio-admin-credentials
key: MINIO_ROOT_PASSWORD
ports:
- containerPort: 9000
- containerPort: 9001
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {} # TODO: Make it persistant
---
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: minio
spec:
selector: {app: minio}
ports:
- name: api
port: 9000
targetPort: 9000
- name: console
port: 9001
targetPort: 9001
+12
View File
@@ -0,0 +1,12 @@
image:
repository: registry.bouvais.lu/tenant/jupyter
tag: "1.0.1"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
+11
View File
@@ -0,0 +1,11 @@
image:
repository: registry.bouvais.lu/tenant/jupyter
tag: "1.0.1"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
+1
View File
@@ -0,0 +1 @@
print("SUCCESS: Python works.")
+3
View File
@@ -0,0 +1,3 @@
import os
import boto3
print("SUCCESS: Import works.")