Compare commits
15 Commits
improvemen
...
a1bc7967c5
| Author | SHA1 | Date | |
|---|---|---|---|
| a1bc7967c5 | |||
| 2a825646a3 | |||
| 7c990998a4 | |||
| bf62ddcbd7 | |||
| 985460ff84 | |||
| 2705f6c16e | |||
| 887247028a | |||
| 4cbd3e2f87 | |||
| 935392d27d | |||
| 30657be6c2 | |||
| 4bca3ae403 | |||
| db80ddf069 | |||
| 0602347539 | |||
| 384c0d58e6 | |||
| 7e96fcef8b |
@@ -34,6 +34,7 @@ WORKDIR /app
|
|||||||
COPY --chown=appuser:appuser . .
|
COPY --chown=appuser:appuser . .
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV IMAGE_TAG=build
|
||||||
USER appuser
|
USER appuser
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
RUN rm -rvf /app/Dockerfile* \
|
RUN rm -rvf /app/Dockerfile* \
|
||||||
@@ -47,5 +48,5 @@ RUN rm -rvf /app/Dockerfile* \
|
|||||||
/app/*.json \
|
/app/*.json \
|
||||||
/app/test_*.py && \
|
/app/test_*.py && \
|
||||||
python3 /app/manage.py collectstatic --noinput
|
python3 /app/manage.py collectstatic --noinput
|
||||||
CMD ["sh", "-c", "python manage.py thumbnail clear && gunicorn --bind 0.0.0.0:8000 --workers 3 labhelper.wsgi:application"]
|
CMD ["sh", "-c", "python manage.py thumbnail clear && gunicorn --bind 0.0.0.0:8000 --workers 3 $GUNICORN_OPTS labhelper.wsgi:application"]
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteMany
|
- ReadWriteMany
|
||||||
storageClassName: nfs-labhelper
|
storageClassName: nfs
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
|
|||||||
21
argocd/configmap.yaml
Normal file
21
argocd/configmap.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: django-config
|
||||||
|
namespace: labhelper
|
||||||
|
data:
|
||||||
|
DEBUG: "False"
|
||||||
|
ALLOWED_HOSTS: "labhelper.adebaumann.com,*"
|
||||||
|
ALLOWED_CIDR_NETS: "10.0.0.0/16"
|
||||||
|
LANGUAGE_CODE: "en-us"
|
||||||
|
TIME_ZONE: "UTC"
|
||||||
|
USE_I18N: "True"
|
||||||
|
USE_TZ: "True"
|
||||||
|
STATIC_URL: "/static/"
|
||||||
|
MEDIA_URL: "/media/"
|
||||||
|
CSRF_TRUSTED_ORIGINS: "https://labhelper.adebaumann.com"
|
||||||
|
LOGIN_URL: "login"
|
||||||
|
LOGIN_REDIRECT_URL: "index"
|
||||||
|
LOGOUT_REDIRECT_URL: "login"
|
||||||
|
GUNICORN_OPTS: "--access-logfile -"
|
||||||
|
IMAGE_TAG: "0.064"
|
||||||
@@ -27,7 +27,7 @@ spec:
|
|||||||
mountPath: /data
|
mountPath: /data
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: git.baumann.gr/adebaumann/labhelper:0.058
|
image: git.baumann.gr/adebaumann/labhelper:0.065
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
@@ -37,6 +37,81 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: django-secret
|
name: django-secret
|
||||||
key: secret-key
|
key: secret-key
|
||||||
|
- name: DEBUG
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: DEBUG
|
||||||
|
- name: ALLOWED_HOSTS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: ALLOWED_HOSTS
|
||||||
|
- name: ALLOWED_CIDR_NETS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: ALLOWED_CIDR_NETS
|
||||||
|
- name: LANGUAGE_CODE
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: LANGUAGE_CODE
|
||||||
|
- name: TIME_ZONE
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: TIME_ZONE
|
||||||
|
- name: USE_I18N
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: USE_I18N
|
||||||
|
- name: USE_TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: USE_TZ
|
||||||
|
- name: STATIC_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: STATIC_URL
|
||||||
|
- name: MEDIA_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: MEDIA_URL
|
||||||
|
- name: CSRF_TRUSTED_ORIGINS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: CSRF_TRUSTED_ORIGINS
|
||||||
|
- name: LOGIN_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: LOGIN_URL
|
||||||
|
- name: LOGIN_REDIRECT_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: LOGIN_REDIRECT_URL
|
||||||
|
- name: LOGOUT_REDIRECT_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: LOGOUT_REDIRECT_URL
|
||||||
|
- name: GUNICORN_OPTS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: GUNICORN_OPTS
|
||||||
|
- name: IMAGE_TAG
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: django-config
|
||||||
|
key: IMAGE_TAG
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /app/data
|
mountPath: /app/data
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ metadata:
|
|||||||
name: labhelper-data-pv
|
name: labhelper-data-pv
|
||||||
namespace: labhelper
|
namespace: labhelper
|
||||||
spec:
|
spec:
|
||||||
|
claimRef:
|
||||||
|
name: labhelper-data-pvc
|
||||||
|
namespace: labhelper
|
||||||
capacity:
|
capacity:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteMany
|
- ReadWriteMany
|
||||||
persistentVolumeReclaimPolicy: Retain
|
persistentVolumeReclaimPolicy: Retain
|
||||||
storageClassName: nfs-labhelper
|
storageClassName: nfs
|
||||||
nfs:
|
nfs:
|
||||||
server: 192.168.17.199
|
server: 192.168.17.199
|
||||||
path: /mnt/user/labhelper
|
path: /mnt/user/labhelper
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
apiVersion: storage.k8s.io/v1
|
|
||||||
kind: StorageClass
|
|
||||||
metadata:
|
|
||||||
name: nfs-labhelper
|
|
||||||
provisioner: kubernetes.io/no-provisioner
|
|
||||||
allowVolumeExpansion: true
|
|
||||||
reclaimPolicy: Retain
|
|
||||||
volumeBindingMode: Immediate
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from adminsortable2.admin import SortableAdminMixin
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
@@ -13,7 +14,7 @@ class BoxFilter(SimpleListFilter):
|
|||||||
parameter_name = 'box__pk'
|
parameter_name = 'box__pk'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
boxes = Box.objects.select_related('box_type').order_by('id')
|
boxes = Box.objects.select_related('box_type').order_by('sort_order')
|
||||||
return [(box.pk, str(box)) for box in boxes]
|
return [(box.pk, str(box)) for box in boxes]
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
@@ -47,7 +48,7 @@ class BoxTypeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(Box)
|
@admin.register(Box)
|
||||||
class BoxAdmin(admin.ModelAdmin):
|
class BoxAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||||
"""Admin configuration for Box model."""
|
"""Admin configuration for Box model."""
|
||||||
|
|
||||||
list_display = ('id', 'box_type')
|
list_display = ('id', 'box_type')
|
||||||
|
|||||||
@@ -4,65 +4,83 @@ from django.conf import settings
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from sorl.thumbnail.models import KVStore
|
from sorl.thumbnail.models import KVStore
|
||||||
from boxes.models import Thing
|
from boxes.models import Thing, ThingFile
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Clean up orphaned images and thumbnails from deleted things'
|
help = "Clean up orphaned images, files, and thumbnails from deleted things"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--dry-run',
|
"--dry-run",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
dest='dry_run',
|
dest="dry_run",
|
||||||
help='Show what would be deleted without actually deleting',
|
help="Show what would be deleted without actually deleting",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
dry_run = options.get('dry_run', False)
|
dry_run = options.get("dry_run", False)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(self.style.WARNING('DRY RUN - No files will be deleted'))
|
self.stdout.write(self.style.WARNING("DRY RUN - No files will be deleted"))
|
||||||
|
|
||||||
self.stdout.write('Finding orphaned images and thumbnails...')
|
self.stdout.write("Finding orphaned images and thumbnails...")
|
||||||
|
|
||||||
media_root = settings.MEDIA_ROOT
|
media_root = settings.MEDIA_ROOT
|
||||||
cache_root = os.path.join(media_root, 'cache')
|
cache_root = os.path.join(media_root, "cache")
|
||||||
things_root = os.path.join(media_root, 'things')
|
things_root = os.path.join(media_root, "things")
|
||||||
|
|
||||||
if not os.path.exists(things_root):
|
if not os.path.exists(things_root):
|
||||||
self.stdout.write(self.style.WARNING('No things directory found'))
|
self.stdout.write(self.style.WARNING("No things directory found"))
|
||||||
return
|
return
|
||||||
|
|
||||||
valid_paths = set()
|
valid_paths = set()
|
||||||
for thing in Thing.objects.exclude(picture__exact='').exclude(picture__isnull=True):
|
for thing in Thing.objects.exclude(picture__exact="").exclude(
|
||||||
|
picture__isnull=True
|
||||||
|
):
|
||||||
if thing.picture:
|
if thing.picture:
|
||||||
valid_paths.add(os.path.basename(thing.picture.name))
|
valid_paths.add(os.path.basename(thing.picture.name))
|
||||||
|
|
||||||
self.stdout.write(f'Found {len(valid_paths)} valid images in database')
|
for thing_file in ThingFile.objects.all():
|
||||||
|
if thing_file.file:
|
||||||
|
if thing_file.file.name.startswith("things/"):
|
||||||
|
relative_path = thing_file.file.name[7:]
|
||||||
|
valid_paths.add(relative_path)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
f"Found {len(valid_paths)} valid images and files in database"
|
||||||
|
)
|
||||||
|
|
||||||
orphaned_thumbnail_paths = set()
|
orphaned_thumbnail_paths = set()
|
||||||
db_cache_paths = set()
|
db_cache_paths = set()
|
||||||
|
|
||||||
for kvstore in KVStore.objects.filter(key__startswith='sorl-thumbnail||image||'):
|
for kvstore in KVStore.objects.filter(
|
||||||
|
key__startswith="sorl-thumbnail||image||"
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
data = json.loads(kvstore.value)
|
data = json.loads(kvstore.value)
|
||||||
name = data.get('name', '')
|
name = data.get("name", "")
|
||||||
if name.startswith('things/'):
|
if name.startswith("things/"):
|
||||||
filename = os.path.basename(name)
|
filename = os.path.basename(name)
|
||||||
if filename not in valid_paths:
|
if filename not in valid_paths:
|
||||||
image_hash = kvstore.key.split('||')[-1]
|
image_hash = kvstore.key.split("||")[-1]
|
||||||
thumbnail_kvstore = KVStore.objects.filter(key=f'sorl-thumbnail||thumbnails||{image_hash}').first()
|
thumbnail_kvstore = KVStore.objects.filter(
|
||||||
|
key=f"sorl-thumbnail||thumbnails||{image_hash}"
|
||||||
|
).first()
|
||||||
if thumbnail_kvstore:
|
if thumbnail_kvstore:
|
||||||
thumbnail_list = json.loads(thumbnail_kvstore.value)
|
thumbnail_list = json.loads(thumbnail_kvstore.value)
|
||||||
for thumbnail_hash in thumbnail_list:
|
for thumbnail_hash in thumbnail_list:
|
||||||
thumbnail_image_kvstore = KVStore.objects.filter(key=f'sorl-thumbnail||image||{thumbnail_hash}').first()
|
thumbnail_image_kvstore = KVStore.objects.filter(
|
||||||
|
key=f"sorl-thumbnail||image||{thumbnail_hash}"
|
||||||
|
).first()
|
||||||
if thumbnail_image_kvstore:
|
if thumbnail_image_kvstore:
|
||||||
thumbnail_data = json.loads(thumbnail_image_kvstore.value)
|
thumbnail_data = json.loads(
|
||||||
thumbnail_path = thumbnail_data.get('name', '')
|
thumbnail_image_kvstore.value
|
||||||
if thumbnail_path.startswith('cache/'):
|
)
|
||||||
|
thumbnail_path = thumbnail_data.get("name", "")
|
||||||
|
if thumbnail_path.startswith("cache/"):
|
||||||
orphaned_thumbnail_paths.add(thumbnail_path)
|
orphaned_thumbnail_paths.add(thumbnail_path)
|
||||||
elif name.startswith('cache/'):
|
elif name.startswith("cache/"):
|
||||||
db_cache_paths.add(name)
|
db_cache_paths.add(name)
|
||||||
except (json.JSONDecodeError, KeyError, AttributeError):
|
except (json.JSONDecodeError, KeyError, AttributeError):
|
||||||
pass
|
pass
|
||||||
@@ -79,26 +97,30 @@ class Command(BaseCommand):
|
|||||||
if relative_path not in valid_paths:
|
if relative_path not in valid_paths:
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(f'Would delete: {file_path}')
|
self.stdout.write(f"Would delete: {file_path}")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.stdout.write(f'Deleted: {file_path}')
|
self.stdout.write(f"Deleted: {file_path}")
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.stdout.write(self.style.ERROR(f'Failed to delete {file_path}: {e}'))
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f"Failed to delete {file_path}: {e}")
|
||||||
|
)
|
||||||
|
|
||||||
for dirname in dirs:
|
for dirname in dirs:
|
||||||
dir_path = os.path.join(root, dirname)
|
dir_path = os.path.join(root, dirname)
|
||||||
if not os.listdir(dir_path):
|
if not os.listdir(dir_path):
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(f'Would remove empty directory: {dir_path}')
|
self.stdout.write(f"Would remove empty directory: {dir_path}")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.rmdir(dir_path)
|
os.rmdir(dir_path)
|
||||||
self.stdout.write(f'Removed empty directory: {dir_path}')
|
self.stdout.write(f"Removed empty directory: {dir_path}")
|
||||||
empty_dirs_removed += 1
|
empty_dirs_removed += 1
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.stdout.write(self.style.ERROR(f'Failed to remove {dir_path}: {e}'))
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f"Failed to remove {dir_path}: {e}")
|
||||||
|
)
|
||||||
|
|
||||||
if os.path.exists(cache_root):
|
if os.path.exists(cache_root):
|
||||||
for root, dirs, files in os.walk(cache_root, topdown=False):
|
for root, dirs, files in os.walk(cache_root, topdown=False):
|
||||||
@@ -109,39 +131,69 @@ class Command(BaseCommand):
|
|||||||
if relative_path in orphaned_thumbnail_paths:
|
if relative_path in orphaned_thumbnail_paths:
|
||||||
thumbnail_deleted_count += 1
|
thumbnail_deleted_count += 1
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(f'Would delete thumbnail (orphaned image): {file_path}')
|
self.stdout.write(
|
||||||
|
f"Would delete thumbnail (orphaned image): {file_path}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.stdout.write(f'Deleted thumbnail (orphaned image): {file_path}')
|
self.stdout.write(
|
||||||
|
f"Deleted thumbnail (orphaned image): {file_path}"
|
||||||
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.stdout.write(self.style.ERROR(f'Failed to delete {file_path}: {e}'))
|
self.stdout.write(
|
||||||
|
self.style.ERROR(
|
||||||
|
f"Failed to delete {file_path}: {e}"
|
||||||
|
)
|
||||||
|
)
|
||||||
elif relative_path not in db_cache_paths:
|
elif relative_path not in db_cache_paths:
|
||||||
thumbnail_deleted_count += 1
|
thumbnail_deleted_count += 1
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(f'Would delete thumbnail (no db entry): {file_path}')
|
self.stdout.write(
|
||||||
|
f"Would delete thumbnail (no db entry): {file_path}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.stdout.write(f'Deleted thumbnail (no db entry): {file_path}')
|
self.stdout.write(
|
||||||
|
f"Deleted thumbnail (no db entry): {file_path}"
|
||||||
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.stdout.write(self.style.ERROR(f'Failed to delete {file_path}: {e}'))
|
self.stdout.write(
|
||||||
|
self.style.ERROR(
|
||||||
|
f"Failed to delete {file_path}: {e}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for dirname in dirs:
|
for dirname in dirs:
|
||||||
dir_path = os.path.join(root, dirname)
|
dir_path = os.path.join(root, dirname)
|
||||||
if not os.listdir(dir_path):
|
if not os.listdir(dir_path):
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(f'Would remove empty cache directory: {dir_path}')
|
self.stdout.write(
|
||||||
|
f"Would remove empty cache directory: {dir_path}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.rmdir(dir_path)
|
os.rmdir(dir_path)
|
||||||
empty_dirs_removed += 1
|
empty_dirs_removed += 1
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.stdout.write(self.style.ERROR(f'Failed to remove {dir_path}: {e}'))
|
self.stdout.write(
|
||||||
|
self.style.ERROR(
|
||||||
|
f"Failed to remove {dir_path}: {e}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
self.stdout.write(self.style.WARNING(f'\nDry run complete. Would delete {deleted_count} images and {thumbnail_deleted_count} thumbnails'))
|
self.stdout.write(
|
||||||
self.stdout.write(f'Would remove {empty_dirs_removed} empty directories')
|
self.style.WARNING(
|
||||||
|
f"\nDry run complete. Would delete {deleted_count} files and {thumbnail_deleted_count} thumbnails"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(f"Would remove {empty_dirs_removed} empty directories")
|
||||||
else:
|
else:
|
||||||
self.stdout.write(self.style.SUCCESS(f'\nCleanup complete! Deleted {deleted_count} images and {thumbnail_deleted_count} thumbnails'))
|
self.stdout.write(
|
||||||
self.stdout.write(f'Removed {empty_dirs_removed} empty directories')
|
self.style.SUCCESS(
|
||||||
|
f"\nCleanup complete! Deleted {deleted_count} files and {thumbnail_deleted_count} thumbnails"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(f"Removed {empty_dirs_removed} empty directories")
|
||||||
|
|||||||
@@ -41,9 +41,15 @@ class Box(models.Model):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='boxes'
|
related_name='boxes'
|
||||||
)
|
)
|
||||||
|
sort_order = models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
db_index=True,
|
||||||
|
help_text='Order in which boxes are displayed'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = 'boxes'
|
verbose_name_plural = 'boxes'
|
||||||
|
ordering = ['sort_order']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.id
|
return self.id
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="thing-image" style="flex-shrink: 0; width: 100%; max-width: 400px;">
|
<div class="thing-image" style="flex-shrink: 0; width: 100%; max-width: 400px;">
|
||||||
{% if thing.picture %}
|
{% if thing.picture %}
|
||||||
{% thumbnail thing.picture "400x400" crop="center" as thumb %}
|
{% thumbnail thing.picture "400x400" crop="center" as thumb %}
|
||||||
<img src="{{ thumb.url }}" alt="{{ thing.name }}" style="width: 100%; height: auto; max-height: 400px; object-fit: cover; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15);">
|
<img class="lightbox-trigger" data-url="{{ thing.picture.url }}" src="{{ thumb.url }}" alt="{{ thing.name }}" style="width: 100%; height: auto; max-height: 400px; object-fit: cover; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); cursor: pointer; transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.02)'" onmouseout="this.style.transform='scale(1)'">
|
||||||
{% endthumbnail %}
|
{% endthumbnail %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="width: 100%; aspect-ratio: 1; max-width: 400px; max-height: 400px; background: linear-gradient(135deg, #e0e0e0 0%, #f0f0f0 100%); display: flex; align-items: center; justify-content: center; color: #999; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15);">
|
<div style="width: 100%; aspect-ratio: 1; max-width: 400px; max-height: 400px; background: linear-gradient(135deg, #e0e0e0 0%, #f0f0f0 100%); display: flex; align-items: center; justify-content: center; color: #999; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15);">
|
||||||
@@ -92,11 +92,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||||
{% for file in thing.files.all %}
|
{% for file in thing.files.all %}
|
||||||
|
{% if file.filename|lower|slice:"-4:" == '.jpg' or file.filename|lower|slice:"-5:" == '.jpeg' or file.filename|lower|slice:"-4:" == '.png' or file.filename|lower|slice:"-5:" == '.webp' or file.filename|lower|slice:"-4:" == '.gif' or file.filename|lower|slice:"-4:" == '.svg' or file.filename|lower|slice:"-4:" == '.bmp' or file.filename|lower|slice:"-5:" == '.tiff' or file.filename|lower|slice:"-4:" == '.ico' %}
|
||||||
|
{% thumbnail file.file "200x200" crop="center" as thumb %}
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border:1px solid #e9ecef;">
|
||||||
|
<img class="lightbox-trigger" data-url="{{ file.file.url }}" src="{{ thumb.url }}" alt="{{ file.title }}" style="width: 60px; height: 60px; object-fit: cover; border-radius: 6px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
||||||
|
<div style="flex-grow: 1;">
|
||||||
|
<a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a>
|
||||||
|
<span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-expand lightbox-trigger" data-url="{{ file.file.url }}" style="color: #999; font-size: 14px; cursor: pointer;"></i>
|
||||||
|
</div>
|
||||||
|
{% endthumbnail %}
|
||||||
|
{% else %}
|
||||||
<div style="display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
|
<div style="display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
|
||||||
<i class="fas fa-paperclip" style="color: #667eea; font-size: 16px;"></i>
|
<i class="fas fa-paperclip" style="color: #667eea; font-size: 16px;"></i>
|
||||||
<a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a>
|
<a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a>
|
||||||
<span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
|
<span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +133,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="lightbox" id="lightbox">
|
||||||
|
<span class="lightbox-close">×</span>
|
||||||
|
<img id="lightbox-image" src="" alt="">
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.lightbox-trigger').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const imageUrl = $(this).data('url');
|
||||||
|
$('#lightbox-image').attr('src', imageUrl);
|
||||||
|
$('#lightbox').addClass('active');
|
||||||
|
$('body').css('overflow', 'hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#lightbox').on('click', function(e) {
|
||||||
|
if (e.target === this || e.target.classList.contains('lightbox-close')) {
|
||||||
|
$('#lightbox').removeClass('active');
|
||||||
|
$('body').css('overflow', 'auto');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
$('#lightbox').removeClass('active');
|
||||||
|
$('body').css('overflow', 'auto');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
@@ -198,5 +244,46 @@
|
|||||||
border-top: 2px solid #e0e0e0;
|
border-top: 2px solid #e0e0e0;
|
||||||
margin: 1.5em 0;
|
margin: 1.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightbox {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
z-index: 9999;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: zoom-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox img {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
color: white;
|
||||||
|
font-size: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10000;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-close:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ def edit_thing(request, thing_id):
|
|||||||
pk=thing_id
|
pk=thing_id
|
||||||
)
|
)
|
||||||
|
|
||||||
boxes = Box.objects.select_related('box_type').all().order_by('id')
|
boxes = Box.objects.select_related('box_type').all()
|
||||||
facets = Facet.objects.all().prefetch_related('tags')
|
facets = Facet.objects.all().prefetch_related('tags')
|
||||||
picture_form = ThingPictureForm(instance=thing)
|
picture_form = ThingPictureForm(instance=thing)
|
||||||
file_form = ThingFileForm()
|
file_form = ThingFileForm()
|
||||||
@@ -192,7 +192,7 @@ def edit_thing(request, thing_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def boxes_list(request):
|
def boxes_list(request):
|
||||||
"""Boxes list page showing all boxes with contents."""
|
"""Boxes list page showing all boxes with contents."""
|
||||||
boxes = Box.objects.select_related('box_type').prefetch_related('things').all().order_by('id')
|
boxes = Box.objects.select_related('box_type').prefetch_related('things').all()
|
||||||
return render(request, 'boxes/boxes_list.html', {
|
return render(request, 'boxes/boxes_list.html', {
|
||||||
'boxes': boxes,
|
'boxes': boxes,
|
||||||
})
|
})
|
||||||
|
|||||||
8
labhelper/context_processors.py
Normal file
8
labhelper/context_processors.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def image_tag(request):
|
||||||
|
"""Add the image tag to all template contexts."""
|
||||||
|
return {
|
||||||
|
'image_tag': os.environ.get('IMAGE_TAG', 'dev')
|
||||||
|
}
|
||||||
@@ -24,10 +24,10 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'f0arjg8q3ut4iuqrguqfjaruf0eripIZZN3t1kymy8ugqnj$li2knhha0@gc5v8f3bge=$+gbybj2$jt28uqm')
|
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'f0arjg8q3ut4iuqrguqfjaruf0eripIZZN3t1kymy8ugqnj$li2knhha0@gc5v8f3bge=$+gbybj2$jt28uqm')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true'
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*","labhelper.adebaumann.com"]
|
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')
|
||||||
ALLOWED_CIDR_NETS = ['10.0.0.0/16']
|
ALLOWED_CIDR_NETS = os.environ.get('ALLOWED_CIDR_NETS', '10.0.0.0/16').split(',')
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@@ -39,6 +39,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'adminsortable2',
|
||||||
'mptt',
|
'mptt',
|
||||||
'django_mptt_admin',
|
'django_mptt_admin',
|
||||||
'sorl.thumbnail',
|
'sorl.thumbnail',
|
||||||
@@ -47,6 +48,7 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@@ -67,6 +69,7 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'labhelper.context_processors.image_tag',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -108,23 +111,26 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = os.environ.get('LANGUAGE_CODE', 'en-us')
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC')
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = os.environ.get('USE_I18N', 'True').lower() == 'true'
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = os.environ.get('USE_TZ', 'True').lower() == 'true'
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = os.environ.get('STATIC_URL', '/static/')
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
|
# WhiteNoise static file serving configuration
|
||||||
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
|
|
||||||
# Media files (user uploads)
|
# Media files (user uploads)
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = os.environ.get('MEDIA_URL', '/media/')
|
||||||
MEDIA_ROOT = BASE_DIR / 'data' / 'media'
|
MEDIA_ROOT = BASE_DIR / 'data' / 'media'
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
@@ -132,8 +138,8 @@ MEDIA_ROOT = BASE_DIR / 'data' / 'media'
|
|||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS=["https://labhelper.adebaumann.com"]
|
CSRF_TRUSTED_ORIGINS=os.environ.get('CSRF_TRUSTED_ORIGINS', 'https://labhelper.adebaumann.com').split(',')
|
||||||
|
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = os.environ.get('LOGIN_URL', 'login')
|
||||||
LOGIN_REDIRECT_URL = 'index'
|
LOGIN_REDIRECT_URL = os.environ.get('LOGIN_REDIRECT_URL', 'index')
|
||||||
LOGOUT_REDIRECT_URL = 'login'
|
LOGOUT_REDIRECT_URL = os.environ.get('LOGOUT_REDIRECT_URL', 'login')
|
||||||
|
|||||||
@@ -435,6 +435,7 @@
|
|||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>© 2025 LabHelper. Built with <i class="fas fa-heart"></i> for science.</p>
|
<p>© 2025 LabHelper. Built with <i class="fas fa-heart"></i> for science.</p>
|
||||||
|
<p style="font-size: 12px; opacity: 0.7; margin-top: 10px;">Image: {{ image_tag }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
|
|||||||
@@ -62,5 +62,17 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
# Static files are served by WhiteNoise middleware in production
|
||||||
|
# Media files need to be served in all environments
|
||||||
|
from django.views.static import serve
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
# Add explicit media serving for production
|
||||||
|
if not settings.DEBUG:
|
||||||
|
urlpatterns += [
|
||||||
|
re_path(r'^media/(?P<path>.*)$', serve, {
|
||||||
|
'document_root': settings.MEDIA_ROOT,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ Pillow==11.1.0
|
|||||||
sorl-thumbnail==12.11.0
|
sorl-thumbnail==12.11.0
|
||||||
bleach==6.1.0
|
bleach==6.1.0
|
||||||
coverage==7.6.1
|
coverage==7.6.1
|
||||||
|
whitenoise==6.8.2
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
NAMESPACE="labhelper"
|
NAMESPACE="labhelper"
|
||||||
SECRET_NAME="django-secret"
|
SECRET_NAME="django-secret"
|
||||||
SECRET_FILE="argocd/secret.yaml"
|
SECRET_FILE="k8s-templates/secret.yaml"
|
||||||
|
|
||||||
# Check if secret file exists
|
# Check if secret file exists
|
||||||
if [ ! -f "$SECRET_FILE" ]; then
|
if [ ! -f "$SECRET_FILE" ]; then
|
||||||
|
|||||||
@@ -36,10 +36,14 @@ NEW_MAIN_VERSION=$(echo "$MAIN_VERSION + 0.001" | bc | sed 's/^\./0./')
|
|||||||
sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION|image: git.baumann.gr/adebaumann/labhelper-data-loader:$NEW_LOADER_VERSION|" "$DEPLOYMENT_FILE"
|
sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION|image: git.baumann.gr/adebaumann/labhelper-data-loader:$NEW_LOADER_VERSION|" "$DEPLOYMENT_FILE"
|
||||||
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
|
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
|
||||||
|
|
||||||
|
# Update ConfigMap with new main container version
|
||||||
|
sed -i "s| IMAGE_TAG: \"$MAIN_VERSION\"| IMAGE_TAG: \"$NEW_MAIN_VERSION\"|" "argocd/configmap.yaml"
|
||||||
|
|
||||||
# Copy database
|
# Copy database
|
||||||
cp "$DB_SOURCE" "$DB_DEST"
|
cp "$DB_SOURCE" "$DB_DEST"
|
||||||
|
|
||||||
echo "Full deployment prepared:"
|
echo "Full deployment prepared:"
|
||||||
echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION"
|
echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION"
|
||||||
echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION"
|
echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION"
|
||||||
|
echo " ConfigMap IMAGE_TAG: $MAIN_VERSION -> $NEW_MAIN_VERSION"
|
||||||
echo " Database copied to $DB_DEST"
|
echo " Database copied to $DB_DEST"
|
||||||
|
|||||||
@@ -23,5 +23,9 @@ NEW_VERSION=$(echo "$CURRENT_VERSION + 0.001" | bc | sed 's/^\./0./')
|
|||||||
# Update the deployment file (only the main container, not the data-loader)
|
# Update the deployment file (only the main container, not the data-loader)
|
||||||
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_VERSION|" "$DEPLOYMENT_FILE"
|
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_VERSION|" "$DEPLOYMENT_FILE"
|
||||||
|
|
||||||
|
# Update ConfigMap with new main container version
|
||||||
|
sed -i "s| IMAGE_TAG: \"$CURRENT_VERSION\"| IMAGE_TAG: \"$NEW_VERSION\"|" "argocd/configmap.yaml"
|
||||||
|
|
||||||
echo "Partial deployment prepared:"
|
echo "Partial deployment prepared:"
|
||||||
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"
|
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"
|
||||||
|
echo " ConfigMap IMAGE_TAG: $CURRENT_VERSION -> $NEW_VERSION"
|
||||||
|
|||||||
Reference in New Issue
Block a user