WIP: can deploy?
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.stamps
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.local
|
||||||
|
docs
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
VITE_TANSTACK_DEVTOOLS=false
|
||||||
+1
-5
@@ -4,10 +4,6 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
.env
|
.env
|
||||||
.nitro
|
|
||||||
.tanstack
|
|
||||||
.wrangler
|
|
||||||
.output
|
|
||||||
.vinxi
|
|
||||||
__unconfig*
|
__unconfig*
|
||||||
todos.json
|
todos.json
|
||||||
|
.stamps/
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:stable-alpine
|
||||||
|
COPY ./dist/. /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
IMAGE_NAME=by-other-means
|
||||||
|
DOCKER_REGISTRY=gitea.pixelparasol.com
|
||||||
|
DOCKER_WORKSPACE=nathan
|
||||||
|
DOCKER_TARGET=$(DOCKER_REGISTRY)/$(DOCKER_WORKSPACE)/$(IMAGE_NAME)
|
||||||
|
K8S_NAMESPACE=by-other-means
|
||||||
|
TAG := $(shell git rev-parse --short HEAD)
|
||||||
|
STAMPS := .stamps
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " npm-install - Force a clean reinstall of npm dependencies"
|
||||||
|
@echo " npm-build - Build the project (skipped if sources unchanged)"
|
||||||
|
@echo " docker-build - Build the Docker image (skipped if unchanged)"
|
||||||
|
@echo " docker-push - Push the Docker image (skipped if already pushed for current commit)"
|
||||||
|
@echo " helm - Deploy or upgrade the Helm chart (skipped if charts and image unchanged)"
|
||||||
|
|
||||||
|
# Force a clean reinstall (explicit use only)
|
||||||
|
.PHONY: npm-install
|
||||||
|
npm-install:
|
||||||
|
@echo "📦 Installing npm dependencies"
|
||||||
|
@rm -rf node_modules
|
||||||
|
@npm ci
|
||||||
|
|
||||||
|
# Smart install: only runs if package files changed
|
||||||
|
node_modules/.package-lock.json: package-lock.json package.json
|
||||||
|
@echo "📦 Installing npm dependencies"
|
||||||
|
@npm ci
|
||||||
|
|
||||||
|
$(STAMPS):
|
||||||
|
@mkdir -p $@
|
||||||
|
|
||||||
|
# Smart build: only runs if source files or deps changed
|
||||||
|
SRC_FILES := $(shell find src -type f 2>/dev/null)
|
||||||
|
$(STAMPS)/npm-build: $(SRC_FILES) node_modules/.package-lock.json | $(STAMPS)
|
||||||
|
@echo "🏗️ Building the project"
|
||||||
|
@npm run build
|
||||||
|
@touch $@
|
||||||
|
|
||||||
|
.PHONY: npm-build
|
||||||
|
npm-build: $(STAMPS)/npm-build
|
||||||
|
|
||||||
|
# Smart docker build: only runs if npm build or Dockerfile changed, per tag
|
||||||
|
$(STAMPS)/docker-build-$(TAG): $(STAMPS)/npm-build Dockerfile | $(STAMPS)
|
||||||
|
@echo "🐳 Building Docker image $(DOCKER_TARGET):$(TAG)"
|
||||||
|
@docker buildx build --no-cache -f Dockerfile . --platform linux/amd64 \
|
||||||
|
--load \
|
||||||
|
-t $(DOCKER_TARGET):$(TAG) \
|
||||||
|
-t $(DOCKER_TARGET):latest
|
||||||
|
@touch $@
|
||||||
|
|
||||||
|
.PHONY: docker-build
|
||||||
|
docker-build: $(STAMPS)/docker-build-$(TAG)
|
||||||
|
|
||||||
|
# Smart push: only pushes if docker image was (re)built and not yet pushed
|
||||||
|
$(STAMPS)/docker-pushed-$(TAG): $(STAMPS)/docker-build-$(TAG) | $(STAMPS)
|
||||||
|
@echo "🔐 Logging into Docker registry $(DOCKER_REGISTRY)"
|
||||||
|
@docker login $(DOCKER_REGISTRY)
|
||||||
|
@echo "🚀 Pushing Docker image $(DOCKER_TARGET):$(TAG) and latest"
|
||||||
|
@docker push $(DOCKER_TARGET):$(TAG)
|
||||||
|
@docker push $(DOCKER_TARGET):latest
|
||||||
|
@touch $@
|
||||||
|
|
||||||
|
.PHONY: docker-push
|
||||||
|
docker-push: $(STAMPS)/docker-pushed-$(TAG)
|
||||||
|
|
||||||
|
# Smart helm deploy: only runs if chart files changed or a new image was pushed
|
||||||
|
HELM_FILES := $(shell find helm -type f 2>/dev/null)
|
||||||
|
$(STAMPS)/helm: $(HELM_FILES) $(STAMPS)/docker-pushed-$(TAG) | $(STAMPS)
|
||||||
|
@echo "🚀 Deploying Helm chart to namespace '$(K8S_NAMESPACE)'"
|
||||||
|
@helm upgrade --install $(K8S_NAMESPACE) ./helm -n $(K8S_NAMESPACE) --create-namespace
|
||||||
|
@touch $@
|
||||||
|
|
||||||
|
.PHONY: helm
|
||||||
|
helm: $(STAMPS)/helm
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
|
# NOTES.txt
|
||||||
|
# deployment.yml
|
||||||
|
# secrets.yml
|
||||||
|
# service.yml
|
||||||
|
# persistent_volume.yml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: helm
|
||||||
|
description: By Other Means Control Timer Helm Chart
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 0.0.1
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
# It is recommended to use it with quotes.
|
||||||
|
appVersion: "0.0.1"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
helm upgrade --install by-other-means ./helm -n by-other-means --create-namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Gitea Auth secret for pulling the container from the private registry (namespace-scoped, so this
|
||||||
|
needs to be created once per namespace):
|
||||||
|
|
||||||
|
```
|
||||||
|
kubectl -n by-other-means \
|
||||||
|
create secret docker-registry gitea-auth \
|
||||||
|
--docker-server=https://gitea.pixelparasol.com \
|
||||||
|
--docker-username=nathan \
|
||||||
|
--docker-password=<redacted> \
|
||||||
|
--docker-email=nathan@pixelparasol.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build the image
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
DOCKER_REGISTRY="gitea.pixelparasol.com/nathan/by-other-means" && CI_COMMIT_SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
docker buildx build --no-cache -f Dockerfile . --platform linux/amd64 -t $DOCKER_REGISTRY:latest -t $DOCKER_REGISTRY:$CI_COMMIT_SHORT_SHA
|
||||||
|
|
||||||
|
docker login gitea.pixelparasol.com
|
||||||
|
|
||||||
|
docker push $DOCKER_REGISTRY:$CI_COMMIT_SHORT_SHA; docker push $DOCKER_REGISTRY:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
You may find yourself in a state where you need to do secret checks to see how they render.
|
||||||
|
adding the `--dry-run=server` argument to your upgrade will render the secret checks.
|
||||||
|
|
||||||
|
```
|
||||||
|
helm upgrade --install by-other-means ./helm -n by-other-means --dry-run=server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helm Diff
|
||||||
|
|
||||||
|
Install the `helm-diff` plugin
|
||||||
|
|
||||||
|
```
|
||||||
|
helm plugin install https://github.com/databus23/helm-diff
|
||||||
|
```
|
||||||
|
|
||||||
|
view the diff if an upgrade were issued
|
||||||
|
|
||||||
|
```
|
||||||
|
helm diff upgrade by-other-means ./helm -n by-other-means
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The app can be deployed by tag by specifying it with `--set`
|
||||||
|
|
||||||
|
```
|
||||||
|
helm upgrade by-other-means ./helm -n by-other-means --set deploy.app.tag="v1.0.4"
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
1. {{ .Release.Name }} Deployment Information:
|
||||||
|
- Release Name: {{ .Release.Name }}
|
||||||
|
- Namespace: {{ .Release.Namespace }}
|
||||||
|
- Chart Name: {{ .Chart.Name }}
|
||||||
|
- Chart Version: {{ .Chart.Version }}
|
||||||
|
|
||||||
|
2. {{ .Release.Name }} Service Information:
|
||||||
|
- Service Name: {{ .Values.system }}-service
|
||||||
|
- Service Type: LoadBalancer
|
||||||
|
- Service Port: 80
|
||||||
|
|
||||||
|
3. Useful Commands:
|
||||||
|
- Check the {{ .Release.Name }} Deployment Status:
|
||||||
|
helm status {{ .Release.Name }}
|
||||||
|
|
||||||
|
- Get Detailed Information about the {{ .Release.Name }} Deployment:
|
||||||
|
helm get all {{ .Release.Name }}
|
||||||
|
|
||||||
|
helm diff
|
||||||
|
|
||||||
|
4. Clean Up:
|
||||||
|
- To uninstall/delete the {{ .Release.Name }} deployment, run:
|
||||||
|
helm uninstall {{ .Release.Name }}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{{- define "system.deployment.component" }}
|
||||||
|
{{ $image := .component.image }}
|
||||||
|
{{ $component := .component }}
|
||||||
|
{{ $release := .release }}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ .component.name }}
|
||||||
|
labels:
|
||||||
|
{{ include "helper.metaLabels" . | indent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{ include "helper.matchLabels" . | indent 8 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{ include "helper.metaLabels" . | indent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- include "helper.affinity" . | indent 6 }}
|
||||||
|
{{- if $image.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: {{ toString $image.imagePullSecrets }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .component.volumes }}
|
||||||
|
{{- range $key, $volume := .component.volumes }}
|
||||||
|
volumes:
|
||||||
|
{{- if eq $volume.type "configMap" }}
|
||||||
|
- name: "{{ $volume.name }}"
|
||||||
|
configMap:
|
||||||
|
name: "{{ $volume.name}}"
|
||||||
|
{{- else }}
|
||||||
|
- name: "{{ $release.Name }}-{{ $component.name }}-pvc"
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: "{{ $release.Name }}-{{ $component.name }}-pvc"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .component.name }}
|
||||||
|
{{ include "helper.deploy_tag" (dict "component" .component "values" .values "release" .release) | indent 10 }}
|
||||||
|
{{- if .component.deployment }}
|
||||||
|
{{- toYaml .component.deployment | trim | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .component.volumes }}
|
||||||
|
{{- range $key, $volume := .component.volumes }}
|
||||||
|
volumeMounts:
|
||||||
|
{{- if eq $volume.type "configMap" }}
|
||||||
|
- name: {{ $volume.name }}
|
||||||
|
mountPath: {{ $volume.mountPath }}
|
||||||
|
subPath: {{ $volume.subPath }}
|
||||||
|
{{- else }}
|
||||||
|
- mountPath: {{ $volume.containerMountPath }}
|
||||||
|
name: "{{ $release.Name }}-{{ $component.name }}-pvc"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{{/*
|
||||||
|
match labels
|
||||||
|
*/}}
|
||||||
|
{{- define "helper.matchLabels" -}}
|
||||||
|
app: {{ .component.name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
meta labels
|
||||||
|
*/}}
|
||||||
|
{{- define "helper.metaLabels" -}}
|
||||||
|
app: {{ .component.name }}
|
||||||
|
tenant: {{ .values.system }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Node Affinity helper
|
||||||
|
*/}}
|
||||||
|
{{- define "helper.affinity" -}}
|
||||||
|
{{- if .component.affinity -}}
|
||||||
|
nodeName: {{ .component.affinity }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Image name and tag helper
|
||||||
|
*/}}
|
||||||
|
{{- define "helper.image" -}}
|
||||||
|
image: "{{ .component.image.repository }}:{{ .component.image.tag }}"
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- /* # returns one of the following
|
||||||
|
# - image: current_release:current_tag from lookup
|
||||||
|
# - image: current_release:deploy_tag from image lookup
|
||||||
|
|
||||||
|
# if this iteration of the loop has a .Values.deploy override tag
|
||||||
|
# - set $new_image to the override tag
|
||||||
|
# else
|
||||||
|
# - set $new_image to the current tag
|
||||||
|
*/ -}}
|
||||||
|
{{- define "helper.deploy_tag" -}}
|
||||||
|
|
||||||
|
{{- $new_deployment := "" -}}
|
||||||
|
{{- $component := .component.name -}}
|
||||||
|
|
||||||
|
{{- $deployment := (lookup "apps/v1" "Deployment" .release.Namespace $component) | default dict }}
|
||||||
|
{{- if $deployment -}}
|
||||||
|
{{- $targetContainerName := $component -}}
|
||||||
|
{{- $targetContainer := dict -}}
|
||||||
|
{{- range $deployment.spec.template.spec.containers }}
|
||||||
|
{{- if eq .name $targetContainerName }}
|
||||||
|
{{- $targetContainer = . -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- range $key, $deploy := .values.deploy -}}
|
||||||
|
{{- if eq $key $component -}}
|
||||||
|
{{- $new_deployment = $deploy.tag -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if $new_deployment -}}
|
||||||
|
image: "{{ .component.image.repository }}:{{ $new_deployment }}"
|
||||||
|
{{- else -}}
|
||||||
|
image: {{ $targetContainer.image }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- else -}}
|
||||||
|
image: "{{ .component.image.repository }}:{{ .component.image.tag }}"
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
|
{{- /* include "helper.var_dump" . */ -}}
|
||||||
|
{{- define "helper.var_dump" -}}
|
||||||
|
{{- . | mustToPrettyJson | printf "\n%s" | fail }}
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{{- define "system.persistent_volume.component" }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: "{{ .release.Name }}-{{ .component.name }}-pv"
|
||||||
|
labels:
|
||||||
|
{{ include "helper.metaLabels" . | indent 4 }}
|
||||||
|
spec:
|
||||||
|
storageClassName: {{ .volume.storageClassName }}
|
||||||
|
persistentVolumeReclaimPolicy: {{ .volume.reclaimPolicy }}
|
||||||
|
capacity:
|
||||||
|
storage: {{ .volume.capacity.storage }}
|
||||||
|
accessModes:
|
||||||
|
{{ toYaml .volume.accessModes }}
|
||||||
|
{{- if .volume.hostPath }}
|
||||||
|
hostPath:
|
||||||
|
path: {{ .volume.hostPath }}
|
||||||
|
{{- end }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: "{{ .release.Name }}-{{ .component.name }}-pvc"
|
||||||
|
labels:
|
||||||
|
{{ include "helper.metaLabels" . | indent 4 }}
|
||||||
|
spec:
|
||||||
|
storageClassName: {{ .volume.storageClassName }}
|
||||||
|
volumeName: "{{ .release.Name }}-{{ .component.name }}-pv"
|
||||||
|
accessModes:
|
||||||
|
{{ toYaml .volume.accessModes }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .volume.capacity.storage }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{{- define "system.service.component" }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ .component.name }}-service
|
||||||
|
labels:
|
||||||
|
{{ include "helper.metaLabels" . | indent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .component.service.type }}
|
||||||
|
selector:
|
||||||
|
app: {{ .component.name }}
|
||||||
|
ports:
|
||||||
|
{{- range .component.service.ports }}
|
||||||
|
- protocol: {{ .protocol }}
|
||||||
|
port: {{ .port }}
|
||||||
|
targetPort: {{ .targetPort }}
|
||||||
|
name: {{ .name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{{- define "system.backup.cronjob" -}}
|
||||||
|
---
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: {{ .system_name }}-{{ .backup.pvc_name }}-backup
|
||||||
|
spec:
|
||||||
|
schedule: {{ required "Values.backups.volumes[n].schedule required in crontab format" .backup.schedule }}
|
||||||
|
successfulJobsHistoryLimit: 1
|
||||||
|
failedJobsHistoryLimit: 1
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
volumes:
|
||||||
|
- name: nfs-backup
|
||||||
|
nfs:
|
||||||
|
server: {{ required "Values.backups.volumes[n].nas_ip required to make nfs connection" .nas_ip }}
|
||||||
|
path: {{ required "Values.backups.volumes[n].nas_path required to store backup to a nfs connection" .backup.nas_path }}
|
||||||
|
|
||||||
|
- name: {{ required "Values.backups.volumes[n].pvc_name required" .backup.pvc_name }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ .backup.pvc_name }}
|
||||||
|
containers:
|
||||||
|
- name: config-backup
|
||||||
|
image: {{ .image | default "alpine" }}
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- date;
|
||||||
|
echo "Creating backup for {{ .system_name }}";
|
||||||
|
echo -e "\nCreating {{ .system_name }} directory if it doesnt already exist";
|
||||||
|
mkdir -p /nas/backup/{{ .system_name }};
|
||||||
|
echo -e "\nProcessing {{ .backup.pvc_name }}";
|
||||||
|
echo " - Creating backup of {{ .backup.pvc_name }} volume";
|
||||||
|
tar -czf /nas/backup/{{ .system_name }}/{{ .backup.pvc_name }}_$(date +%Y-%m-%d).tar.gz /backup_source;
|
||||||
|
echo " - Enforcing backup retention policy for {{ .backup.pvc_name }} to latest {{ required "Values.backups.volumes[n].retention required as an integer" .backup.retention }}";
|
||||||
|
ls -t /nas/backup/{{ required "Values.backups.system_name required" .system_name }}/{{ .backup.pvc_name }}_*.tar.gz | sort -r | tail -n +{{ add1 .backup.retention }} | xargs rm -f;
|
||||||
|
echo -e "\nCompleted backup of {{ .backup.pvc_name }}";
|
||||||
|
date;
|
||||||
|
|
||||||
|
volumeMounts:
|
||||||
|
- name: nfs-backup
|
||||||
|
mountPath: /nas/backup
|
||||||
|
- name: {{ .backup.pvc_name }}
|
||||||
|
mountPath: /backup_source
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ if .Values.backups.enabled }}
|
||||||
|
{{- range $parent, $backup := .Values.backups.volumes -}}
|
||||||
|
{{- if $backup.enabled }}
|
||||||
|
{{ include "system.backup.cronjob" (dict "backup" $backup "image" $.Values.backups.image "nas_ip" $.Values.backups.nas_ip "system_name" $.Values.backups.system_name) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{{- range $system, $settings := .Values.components -}}
|
||||||
|
{{ include "system.deployment.component" (dict "component" $settings "values" $.Values "release" $.Release) }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.system }}-ingress
|
||||||
|
labels:
|
||||||
|
app: {{ .Values.system }}
|
||||||
|
tenant: {{ .Values.system }}
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: lets-encrypt-prod
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ .Values.components.app.ingress.className }}
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- "{{ .Values.components.app.ingress.domain }}"
|
||||||
|
secretName: {{ .Values.system }}-ingress-tls
|
||||||
|
rules:
|
||||||
|
- host: "{{ .Values.components.app.ingress.domain }}"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: {{ .Values.components.app.ingress.path }}
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ .Values.system }}-service
|
||||||
|
port:
|
||||||
|
number: {{ .Values.components.app.ingress.port }}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: nginx-configmap
|
||||||
|
labels:
|
||||||
|
app: {{ .Values.system }}
|
||||||
|
tenant: {{ .Values.system }}
|
||||||
|
data:
|
||||||
|
default.conf: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{{- range $system, $settings := .Values.components -}}
|
||||||
|
{{ if $settings.volumes }}
|
||||||
|
{{- range $component, $volume := $settings.volumes -}}
|
||||||
|
{{- if ne $volume.type "configMap" -}}
|
||||||
|
{{ include "system.persistent_volume.component" (dict "component" $settings "volume" $volume "values" $.Values "release" $.Release) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
{{/*
|
||||||
|
Secret initializer and data maintainer
|
||||||
|
This helper will initialize the secret data with "initial" value if the secret is not found in the secret object.
|
||||||
|
IF the secret is found in the secret object, it will use the set value from the secret object thereby maintaining the secret data.
|
||||||
|
*/}}
|
||||||
|
{{- define "helper.secrets" -}}
|
||||||
|
{{- $secretObj := (lookup "v1" "Secret" .release.Namespace .component.name) | default dict }}
|
||||||
|
{{- range $key, $value := .component.secrets }}
|
||||||
|
{{- if empty $value -}}
|
||||||
|
{{- $secretData := (get $secretObj "data") | default dict }}
|
||||||
|
{{- $secret := (get $secretData $key) | default ("initial" | b64enc) }}
|
||||||
|
{{ $key }}: {{ $secret | quote }}
|
||||||
|
{{- else }}
|
||||||
|
{{ $key }}: {{ $value | b64enc | quote }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- range $system, $settings := .Values.components -}}
|
||||||
|
{{ if $settings.secrets }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ $settings.name }}
|
||||||
|
labels:
|
||||||
|
{{ include "helper.metaLabels" (dict "component" $settings "values" $.Values) | indent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
{{- include "helper.secrets" (dict "component" $settings "release" $.Release) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{- range $system, $settings := .Values.components -}}
|
||||||
|
{{ if $settings.service }}
|
||||||
|
{{ include "system.service.component" (dict "component" $settings "values" $.Values) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
system: by-other-means
|
||||||
|
|
||||||
|
components:
|
||||||
|
app:
|
||||||
|
name: by-other-means
|
||||||
|
|
||||||
|
affinity: null
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: gitea.pixelparasol.com/nathan/by-other-means
|
||||||
|
tag: "latest"
|
||||||
|
imagePullPolicy: "Always"
|
||||||
|
imagePullSecrets: "gitea-auth"
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
tls: true
|
||||||
|
domain: bom.nathanbstanley.com
|
||||||
|
path: /
|
||||||
|
className: "nginx"
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
name: http
|
||||||
|
|
||||||
|
deployment:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: nginx-configmap
|
||||||
|
type: configMap
|
||||||
|
mountPath: "/etc/nginx/conf.d/default.conf"
|
||||||
|
subPath: "default.conf"
|
||||||
|
|
||||||
|
backups:
|
||||||
|
enabled: false
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>By Other Means Control Timer</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-900 text-slate-100">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+91
-655
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -7,7 +7,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3000",
|
"dev": "vite dev --port 3000",
|
||||||
"generate-routes": "tsr generate",
|
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
"@tanstack/react-devtools": "latest",
|
"@tanstack/react-devtools": "latest",
|
||||||
"@tanstack/react-router": "latest",
|
"@tanstack/react-router": "latest",
|
||||||
"@tanstack/react-router-devtools": "latest",
|
"@tanstack/react-router-devtools": "latest",
|
||||||
"@tanstack/react-start": "latest",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -31,7 +29,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.5",
|
"@biomejs/biome": "2.4.5",
|
||||||
"@tanstack/devtools-vite": "latest",
|
"@tanstack/devtools-vite": "latest",
|
||||||
"@tanstack/router-cli": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"@types/luxon": "^3.7.2",
|
"@types/luxon": "^3.7.2",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { RouterProvider } from "@tanstack/react-router";
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import { getRouter } from "#/router";
|
||||||
|
import { persistor, store } from "#/store/store";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
const router = getRouter();
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("app");
|
||||||
|
if (!rootElement)
|
||||||
|
throw new Error("Could not find #app element to mount the app");
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
+17
-3
@@ -1,5 +1,11 @@
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { type FC, useEffect, useState } from "react";
|
import {
|
||||||
|
type EventHandler,
|
||||||
|
type FC,
|
||||||
|
type MouseEventHandler,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../store/hooks";
|
import { useAppDispatch, useAppSelector } from "../store/hooks";
|
||||||
import { clearStartTime, setStartTime } from "../store/slices/timeSlice";
|
import { clearStartTime, setStartTime } from "../store/slices/timeSlice";
|
||||||
import { ROUND_LENGTH_SECONDS } from "../utils/constants";
|
import { ROUND_LENGTH_SECONDS } from "../utils/constants";
|
||||||
@@ -22,6 +28,14 @@ const Welcome: FC = () => {
|
|||||||
const isRunning = startTime !== null;
|
const isRunning = startTime !== null;
|
||||||
const isOvertime = elapsedSeconds > 60;
|
const isOvertime = elapsedSeconds > 60;
|
||||||
|
|
||||||
|
const nextRoundClick: MouseEventHandler<HTMLButtonElement> = (_e) => {
|
||||||
|
dispatch(clearStartTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRoundClick: MouseEventHandler<HTMLButtonElement> = (_e) => {
|
||||||
|
dispatch(setStartTime(DateTime.now().toISO() ?? ""));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
|
<div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
|
||||||
<h1 className="text-4xl font-bold">By Other Means Control Timer</h1>
|
<h1 className="text-4xl font-bold">By Other Means Control Timer</h1>
|
||||||
@@ -35,7 +49,7 @@ const Welcome: FC = () => {
|
|||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => dispatch(clearStartTime())}
|
onClick={nextRoundClick}
|
||||||
className="rounded-lg bg-slate-700 px-6 py-3 text-lg font-semibold hover:bg-slate-600"
|
className="rounded-lg bg-slate-700 px-6 py-3 text-lg font-semibold hover:bg-slate-600"
|
||||||
>
|
>
|
||||||
Prepare Next Round
|
Prepare Next Round
|
||||||
@@ -43,7 +57,7 @@ const Welcome: FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => dispatch(setStartTime(DateTime.now().toISO() ?? ""))}
|
onClick={startRoundClick}
|
||||||
className="rounded-lg bg-emerald-600 px-6 py-3 text-lg font-semibold hover:bg-emerald-500"
|
className="rounded-lg bg-emerald-600 px-6 py-3 text-lg font-semibold hover:bg-emerald-500"
|
||||||
>
|
>
|
||||||
Start Round
|
Start Round
|
||||||
|
|||||||
@@ -57,12 +57,3 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
import type { getRouter } from './router.tsx'
|
|
||||||
import type { createStart } from '@tanstack/react-start'
|
|
||||||
declare module '@tanstack/react-start' {
|
|
||||||
interface Register {
|
|
||||||
ssr: true
|
|
||||||
router: Awaited<ReturnType<typeof getRouter>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+6
-41
@@ -1,48 +1,15 @@
|
|||||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||||
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
|
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import { PersistGate } from "redux-persist/integration/react";
|
|
||||||
|
|
||||||
import { persistor, store } from "#/store/store";
|
|
||||||
import appCss from "../styles.css?url";
|
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
head: () => ({
|
component: RootComponent,
|
||||||
meta: [
|
|
||||||
{
|
|
||||||
charSet: "utf-8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "viewport",
|
|
||||||
content: "width=device-width, initial-scale=1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "By Other Means Control Timer",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
rel: "stylesheet",
|
|
||||||
href: appCss,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
shellComponent: RootDocument,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
function RootComponent() {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<>
|
||||||
<head>
|
<Outlet />
|
||||||
<HeadContent />
|
|
||||||
</head>
|
|
||||||
<body className="bg-slate-900 text-slate-100">
|
|
||||||
<Provider store={store}>
|
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
|
||||||
{children}
|
|
||||||
</PersistGate>
|
|
||||||
</Provider>
|
|
||||||
{import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && (
|
{import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && (
|
||||||
<TanStackDevtools
|
<TanStackDevtools
|
||||||
config={{
|
config={{
|
||||||
@@ -56,8 +23,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Scripts />
|
</>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -14,9 +14,9 @@ const noopStorage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// redux-persist's bundled storage needs `localStorage`, which isn't available
|
// redux-persist's bundled storage engine can throw or come back malformed in
|
||||||
// during server-side rendering (and can throw in locked-down browsers), so
|
// some browsers (private mode, locked-down storage), so this is a minimal
|
||||||
// this is a minimal storage engine that never touches `redux-persist/lib/storage`.
|
// storage engine that never touches `redux-persist/lib/storage`.
|
||||||
function createLocalStorageEngine() {
|
function createLocalStorageEngine() {
|
||||||
if (typeof window === "undefined") return noopStorage;
|
if (typeof window === "undefined") return noopStorage;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"target": "react"
|
|
||||||
}
|
|
||||||
+7
-4
@@ -1,14 +1,17 @@
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { devtools } from "@tanstack/devtools-vite";
|
import { devtools } from "@tanstack/devtools-vite";
|
||||||
|
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
||||||
|
|
||||||
import viteReact from "@vitejs/plugin-react";
|
import viteReact from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
const config = defineConfig({
|
const config = defineConfig({
|
||||||
resolve: { tsconfigPaths: true },
|
resolve: { tsconfigPaths: true },
|
||||||
plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()],
|
plugins: [
|
||||||
|
devtools(),
|
||||||
|
tailwindcss(),
|
||||||
|
tanstackRouter({ target: "react", autoCodeSplitting: true }),
|
||||||
|
viteReact(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user