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
|
||||
*.local
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
.output
|
||||
.vinxi
|
||||
__unconfig*
|
||||
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": {
|
||||
"dev": "vite dev --port 3000",
|
||||
"generate-routes": "tsr generate",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"format": "biome format --write .",
|
||||
@@ -20,7 +19,6 @@
|
||||
"@tanstack/react-devtools": "latest",
|
||||
"@tanstack/react-router": "latest",
|
||||
"@tanstack/react-router-devtools": "latest",
|
||||
"@tanstack/react-start": "latest",
|
||||
"luxon": "^3.7.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -31,7 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@tanstack/devtools-vite": "latest",
|
||||
"@tanstack/router-cli": "^1.132.0",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"@types/luxon": "^3.7.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@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 { type FC, useEffect, useState } from "react";
|
||||
import {
|
||||
type EventHandler,
|
||||
type FC,
|
||||
type MouseEventHandler,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../store/hooks";
|
||||
import { clearStartTime, setStartTime } from "../store/slices/timeSlice";
|
||||
import { ROUND_LENGTH_SECONDS } from "../utils/constants";
|
||||
@@ -22,6 +28,14 @@ const Welcome: FC = () => {
|
||||
const isRunning = startTime !== null;
|
||||
const isOvertime = elapsedSeconds > 60;
|
||||
|
||||
const nextRoundClick: MouseEventHandler<HTMLButtonElement> = (_e) => {
|
||||
dispatch(clearStartTime());
|
||||
};
|
||||
|
||||
const startRoundClick: MouseEventHandler<HTMLButtonElement> = (_e) => {
|
||||
dispatch(setStartTime(DateTime.now().toISO() ?? ""));
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
@@ -35,7 +49,7 @@ const Welcome: FC = () => {
|
||||
{isRunning ? (
|
||||
<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"
|
||||
>
|
||||
Prepare Next Round
|
||||
@@ -43,7 +57,7 @@ const Welcome: FC = () => {
|
||||
) : (
|
||||
<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"
|
||||
>
|
||||
Start Round
|
||||
|
||||
@@ -57,12 +57,3 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._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>>
|
||||
}
|
||||
}
|
||||
|
||||
+19
-54
@@ -1,63 +1,28 @@
|
||||
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 { 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({
|
||||
head: () => ({
|
||||
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,
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
function RootComponent() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" && (
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
<>
|
||||
<Outlet />
|
||||
{import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && (
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -14,9 +14,9 @@ const noopStorage = {
|
||||
},
|
||||
};
|
||||
|
||||
// redux-persist's bundled storage needs `localStorage`, which isn't available
|
||||
// during server-side rendering (and can throw in locked-down browsers), so
|
||||
// this is a minimal storage engine that never touches `redux-persist/lib/storage`.
|
||||
// redux-persist's bundled storage engine can throw or come back malformed in
|
||||
// some browsers (private mode, locked-down storage), so this is a minimal
|
||||
// storage engine that never touches `redux-persist/lib/storage`.
|
||||
function createLocalStorageEngine() {
|
||||
if (typeof window === "undefined") return noopStorage;
|
||||
try {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"target": "react"
|
||||
}
|
||||
+7
-4
@@ -1,14 +1,17 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { devtools } from "@tanstack/devtools-vite";
|
||||
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: { tsconfigPaths: true },
|
||||
plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()],
|
||||
plugins: [
|
||||
devtools(),
|
||||
tailwindcss(),
|
||||
tanstackRouter({ target: "react", autoCodeSplitting: true }),
|
||||
viteReact(),
|
||||
],
|
||||
});
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user