Compare commits
1 Commits
2e26b24e7b
...
renovate/o
| Author | SHA1 | Date | |
|---|---|---|---|
| 977722f1b5 |
@@ -1,55 +0,0 @@
|
|||||||
when:
|
|
||||||
- event: push
|
|
||||||
branch: fresh-start
|
|
||||||
|
|
||||||
skip_clone: true
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Get kubernetes access from OpenBao
|
|
||||||
image: quay.io/openbao/openbao:2.5.2
|
|
||||||
volumes:
|
|
||||||
- secrets:/secrets
|
|
||||||
environment:
|
|
||||||
ROLE_ID:
|
|
||||||
from_secret: flux_reconcile_role_id
|
|
||||||
SECRET_ID:
|
|
||||||
from_secret: flux_reconcile_secret_id
|
|
||||||
commands:
|
|
||||||
- bao write -field token auth/approle/login
|
|
||||||
role_id=$ROLE_ID
|
|
||||||
secret_id=$SECRET_ID
|
|
||||||
\> /secrets/.vault_id
|
|
||||||
- export VAULT_TOKEN=$(cat /secrets/.vault_id)
|
|
||||||
- bao write -format json /kubernetes/creds/flux-reconcile
|
|
||||||
\> /secrets/kube_credentials
|
|
||||||
- bao read -format
|
|
||||||
- name: Construct Kubeconfig
|
|
||||||
image: alpine/k8s:1.32.13
|
|
||||||
volumes:
|
|
||||||
- secrets:/secrets
|
|
||||||
environment:
|
|
||||||
KUBECONFIG: /secrets/kubeconfig
|
|
||||||
commands:
|
|
||||||
- kubectl config set-cluster cluster
|
|
||||||
--server=https://$KUBERNETES_SERVICE_HOST
|
|
||||||
--client-certificate=/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
|
||||||
- kubectl config set-credentials cluster
|
|
||||||
--token=$(jq -r .data.service_account_token /secrets/kube_credentials)
|
|
||||||
- kubectl config set-context cluster
|
|
||||||
--cluster cluster
|
|
||||||
--user cluster
|
|
||||||
--namespace flux-system
|
|
||||||
--current=true
|
|
||||||
- name: Reconcile git source
|
|
||||||
image: ghcr.io/fluxcd/flux-cli:v2.8.3
|
|
||||||
volumes:
|
|
||||||
- secrets:/secrets
|
|
||||||
environment:
|
|
||||||
KUBECONFIG: /secrets/kubeconfig
|
|
||||||
commands:
|
|
||||||
- flux reconcile source git flux-system
|
|
||||||
- name: Invalidate OpenBao token
|
|
||||||
image: quay.io/openbao/openbao:2.5.2
|
|
||||||
commands:
|
|
||||||
- export VAULT_TOKEN=$(cat /secrets/.vault_id)
|
|
||||||
- bao write auth/token/revoke-self
|
|
||||||
15
.woodpecker/my-first-workflow.yaml
Normal file
15
.woodpecker/my-first-workflow.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: fresh-start
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: debian
|
||||||
|
commands:
|
||||||
|
- echo "This is the build step"
|
||||||
|
- echo "echo hello world" > executable
|
||||||
|
- name: a-test-step
|
||||||
|
image: golang:1.16
|
||||||
|
commands:
|
||||||
|
- echo "Testing ..."
|
||||||
|
- sh executable
|
||||||
18
Makefile
18
Makefile
@@ -1,6 +1,6 @@
|
|||||||
SHELL := /usr/bin/env bash
|
SHELL := /usr/bin/env bash
|
||||||
|
|
||||||
.PHONY: install-router gen-talos-config apply-talos-config get-kubeconfig
|
.PHONY: install-router gen-talos-config apply-talos-config get-kubeconfig garm-image-build garm-image-push garm-image-build-push
|
||||||
|
|
||||||
install-router:
|
install-router:
|
||||||
ansible-playbook ansible/playbook.yml -i ansible/hosts
|
ansible-playbook ansible/playbook.yml -i ansible/hosts
|
||||||
@@ -27,3 +27,19 @@ apply-talos-config:
|
|||||||
|
|
||||||
get-kubeconfig:
|
get-kubeconfig:
|
||||||
talosctl -n anapistula-delrosalae kubeconfig talos/generated/kubeconfig
|
talosctl -n anapistula-delrosalae kubeconfig talos/generated/kubeconfig
|
||||||
|
|
||||||
|
garm-image-build:
|
||||||
|
set -euo pipefail; \
|
||||||
|
source apps/garm/image-source.env; \
|
||||||
|
docker build \
|
||||||
|
-f docker/garm/Dockerfile \
|
||||||
|
--build-arg GARM_COMMIT=$$GARM_COMMIT \
|
||||||
|
-t $$GARM_IMAGE \
|
||||||
|
.
|
||||||
|
|
||||||
|
garm-image-push:
|
||||||
|
set -euo pipefail; \
|
||||||
|
source apps/garm/image-source.env; \
|
||||||
|
docker push $$GARM_IMAGE
|
||||||
|
|
||||||
|
garm-image-build-push: garm-image-build garm-image-push
|
||||||
|
|||||||
49
apps/garm/README.md
Normal file
49
apps/garm/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# garm
|
||||||
|
|
||||||
|
This app deploys `garm` with external `garm-provider-k8s`.
|
||||||
|
|
||||||
|
- API/UI ingress: `https://garm.lumpiasty.xyz`
|
||||||
|
- Internal service DNS: `http://garm.garm.svc.cluster.local:9997`
|
||||||
|
|
||||||
|
## Vault secret requirements
|
||||||
|
|
||||||
|
`VaultStaticSecret` reads `secret/data/garm` and expects at least:
|
||||||
|
|
||||||
|
- `jwt_auth_secret`
|
||||||
|
- `database_passphrase` (must be 32 characters)
|
||||||
|
|
||||||
|
## Connect garm to Gitea
|
||||||
|
|
||||||
|
After Flux reconciles this app, initialize garm and add Gitea endpoint/credentials.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Initialize garm (from your local devenv shell)
|
||||||
|
garm-cli init \
|
||||||
|
--name homelab \
|
||||||
|
--url https://garm.lumpiasty.xyz \
|
||||||
|
--username admin \
|
||||||
|
--email admin@lumpiasty.xyz \
|
||||||
|
--password '<STRONG_ADMIN_PASSWORD>' \
|
||||||
|
--metadata-url http://garm.garm.svc.cluster.local:9997/api/v1/metadata \
|
||||||
|
--callback-url http://garm.garm.svc.cluster.local:9997/api/v1/callbacks \
|
||||||
|
--webhook-url http://garm.garm.svc.cluster.local:9997/webhooks
|
||||||
|
|
||||||
|
# 2) Add Gitea endpoint
|
||||||
|
garm-cli gitea endpoint create \
|
||||||
|
--name local-gitea \
|
||||||
|
--description 'Cluster Gitea' \
|
||||||
|
--base-url http://gitea-http.gitea.svc.cluster.local:80 \
|
||||||
|
--api-base-url http://gitea-http.gitea.svc.cluster.local:80/api/v1
|
||||||
|
|
||||||
|
# 3) Add Gitea PAT credentials
|
||||||
|
garm-cli gitea credentials add \
|
||||||
|
--name gitea-pat \
|
||||||
|
--description 'PAT for garm' \
|
||||||
|
--endpoint local-gitea \
|
||||||
|
--auth-type pat \
|
||||||
|
--pat-oauth-token '<GITEA_PAT_WITH_write:repository,write:organization>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add repositories/orgs and create pools against provider `kubernetes_external`.
|
||||||
|
|
||||||
|
If Gitea refuses webhook installation to cluster-local URLs, set `gitea.config.webhook.ALLOWED_HOST_LIST` in `apps/gitea/release.yaml`.
|
||||||
19
apps/garm/configmap.yaml
Normal file
19
apps/garm/configmap.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: garm-provider-k8s-config
|
||||||
|
namespace: garm
|
||||||
|
data:
|
||||||
|
provider-config.yaml: |
|
||||||
|
kubeConfigPath: ""
|
||||||
|
runnerNamespace: "garm-runners"
|
||||||
|
podTemplate:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
flavors:
|
||||||
|
default:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
106
apps/garm/deployment.yaml
Normal file
106
apps/garm/deployment.yaml
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: garm
|
||||||
|
namespace: garm
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: garm
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: garm
|
||||||
|
spec:
|
||||||
|
serviceAccountName: garm
|
||||||
|
initContainers:
|
||||||
|
- name: render-garm-config
|
||||||
|
image: alpine:3.23
|
||||||
|
env:
|
||||||
|
- name: JWT_AUTH_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: garm-config
|
||||||
|
key: jwt_auth_secret
|
||||||
|
- name: DATABASE_PASSPHRASE
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: garm-config
|
||||||
|
key: database_passphrase
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
cat <<EOF > /etc/garm/config.toml
|
||||||
|
[default]
|
||||||
|
enable_webhook_management = true
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
enable_log_streamer = true
|
||||||
|
log_format = "text"
|
||||||
|
log_level = "info"
|
||||||
|
log_source = false
|
||||||
|
|
||||||
|
[metrics]
|
||||||
|
enable = true
|
||||||
|
disable_auth = false
|
||||||
|
|
||||||
|
[jwt_auth]
|
||||||
|
secret = "${JWT_AUTH_SECRET}"
|
||||||
|
time_to_live = "8760h"
|
||||||
|
|
||||||
|
[apiserver]
|
||||||
|
bind = "0.0.0.0"
|
||||||
|
port = 9997
|
||||||
|
use_tls = false
|
||||||
|
[apiserver.webui]
|
||||||
|
enable = true
|
||||||
|
|
||||||
|
[database]
|
||||||
|
backend = "sqlite3"
|
||||||
|
passphrase = "${DATABASE_PASSPHRASE}"
|
||||||
|
[database.sqlite3]
|
||||||
|
db_file = "/data/garm.db"
|
||||||
|
busy_timeout_seconds = 5
|
||||||
|
|
||||||
|
[[provider]]
|
||||||
|
name = "kubernetes_external"
|
||||||
|
description = "Kubernetes provider"
|
||||||
|
provider_type = "external"
|
||||||
|
[provider.external]
|
||||||
|
config_file = "/etc/garm/provider-config.yaml"
|
||||||
|
provider_executable = "/opt/garm/providers.d/garm-provider-k8s"
|
||||||
|
environment_variables = ["KUBERNETES_"]
|
||||||
|
EOF
|
||||||
|
volumeMounts:
|
||||||
|
- name: config-dir
|
||||||
|
mountPath: /etc/garm
|
||||||
|
containers:
|
||||||
|
- name: garm
|
||||||
|
image: gitea.lumpiasty.xyz/lumpiasty/garm-k8s:r1380
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /bin/garm
|
||||||
|
- --config
|
||||||
|
- /etc/garm/config.toml
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 9997
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
- name: config-dir
|
||||||
|
mountPath: /etc/garm
|
||||||
|
- name: provider-config
|
||||||
|
mountPath: /etc/garm/provider-config.yaml
|
||||||
|
subPath: provider-config.yaml
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: garm-lvmhdd
|
||||||
|
- name: config-dir
|
||||||
|
emptyDir: {}
|
||||||
|
- name: provider-config
|
||||||
|
configMap:
|
||||||
|
name: garm-provider-k8s-config
|
||||||
5
apps/garm/image-source.env
Normal file
5
apps/garm/image-source.env
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# renovate: datasource=github-refs depName=cloudbase/garm versioning=git
|
||||||
|
GARM_COMMIT=818a9dddccba5f2843f185e6a846770988f31fc5
|
||||||
|
GARM_COMMIT_NUMBER=1380
|
||||||
|
GARM_IMAGE_REPO=gitea.lumpiasty.xyz/lumpiasty/garm-k8s
|
||||||
|
GARM_IMAGE=gitea.lumpiasty.xyz/lumpiasty/garm-k8s:r1380
|
||||||
24
apps/garm/ingress.yaml
Normal file
24
apps/garm/ingress.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
namespace: garm
|
||||||
|
name: garm
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx-ingress
|
||||||
|
rules:
|
||||||
|
- host: garm.lumpiasty.xyz
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: garm
|
||||||
|
port:
|
||||||
|
number: 9997
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- garm.lumpiasty.xyz
|
||||||
|
secretName: garm-ingress
|
||||||
11
apps/garm/kustomization.yaml
Normal file
11
apps/garm/kustomization.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- configmap.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
9
apps/garm/namespace.yaml
Normal file
9
apps/garm/namespace.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: garm
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: garm-runners
|
||||||
46
apps/garm/pvc.yaml
Normal file
46
apps/garm/pvc.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
apiVersion: local.openebs.io/v1alpha1
|
||||||
|
kind: LVMVolume
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
kubernetes.io/nodename: anapistula-delrosalae
|
||||||
|
name: garm-lvmhdd
|
||||||
|
namespace: openebs
|
||||||
|
spec:
|
||||||
|
capacity: 5Gi
|
||||||
|
ownerNodeID: anapistula-delrosalae
|
||||||
|
shared: "yes"
|
||||||
|
thinProvision: "no"
|
||||||
|
vgPattern: ^openebs-hdd$
|
||||||
|
volGroup: openebs-hdd
|
||||||
|
---
|
||||||
|
kind: PersistentVolume
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: garm-lvmhdd
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 5Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: hdd-lvmpv
|
||||||
|
volumeMode: Filesystem
|
||||||
|
csi:
|
||||||
|
driver: local.csi.openebs.io
|
||||||
|
fsType: btrfs
|
||||||
|
volumeHandle: garm-lvmhdd
|
||||||
|
---
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: garm-lvmhdd
|
||||||
|
namespace: garm
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
storageClassName: hdd-lvmpv
|
||||||
|
volumeName: garm-lvmhdd
|
||||||
51
apps/garm/rbac.yaml
Normal file
51
apps/garm/rbac.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: garm
|
||||||
|
namespace: garm
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: garm-provider-k8s
|
||||||
|
namespace: garm-runners
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods", "pods/log", "configmaps", "secrets", "events"]
|
||||||
|
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: garm-provider-k8s
|
||||||
|
namespace: garm-runners
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: garm
|
||||||
|
namespace: garm
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: garm-provider-k8s
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: garm-namespace-manager
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["namespaces"]
|
||||||
|
verbs: ["get"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: garm-namespace-manager
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: garm
|
||||||
|
namespace: garm
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: garm-namespace-manager
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
32
apps/garm/secret.yaml
Normal file
32
apps/garm/secret.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
kind: VaultAuth
|
||||||
|
metadata:
|
||||||
|
name: garm
|
||||||
|
namespace: garm
|
||||||
|
spec:
|
||||||
|
method: kubernetes
|
||||||
|
mount: kubernetes
|
||||||
|
kubernetes:
|
||||||
|
role: garm
|
||||||
|
serviceAccount: garm
|
||||||
|
---
|
||||||
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
kind: VaultStaticSecret
|
||||||
|
metadata:
|
||||||
|
name: garm-config
|
||||||
|
namespace: garm
|
||||||
|
spec:
|
||||||
|
type: kv-v2
|
||||||
|
|
||||||
|
mount: secret
|
||||||
|
path: garm
|
||||||
|
|
||||||
|
destination:
|
||||||
|
create: true
|
||||||
|
name: garm-config
|
||||||
|
type: Opaque
|
||||||
|
transformation:
|
||||||
|
excludeRaw: true
|
||||||
|
|
||||||
|
vaultAuthRef: garm
|
||||||
14
apps/garm/service.yaml
Normal file
14
apps/garm/service.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: garm
|
||||||
|
namespace: garm
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: garm
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 9997
|
||||||
|
targetPort: 9997
|
||||||
|
protocol: TCP
|
||||||
@@ -73,7 +73,7 @@ spec:
|
|||||||
ISSUE_INDEXER_TYPE: bleve
|
ISSUE_INDEXER_TYPE: bleve
|
||||||
REPO_INDEXER_ENABLED: true
|
REPO_INDEXER_ENABLED: true
|
||||||
webhook:
|
webhook:
|
||||||
ALLOWED_HOST_LIST: woodpecker.lumpiasty.xyz
|
ALLOWED_HOST_LIST: garm.garm.svc.cluster.local,woodpecker.lumpiasty.xyz
|
||||||
admin:
|
admin:
|
||||||
username: GiteaAdmin
|
username: GiteaAdmin
|
||||||
email: gi@tea.com
|
email: gi@tea.com
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ resources:
|
|||||||
- searxng
|
- searxng
|
||||||
- ispeak3
|
- ispeak3
|
||||||
- openwebui
|
- openwebui
|
||||||
|
- garm
|
||||||
- woodpecker
|
- woodpecker
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: llama-swap
|
- name: llama-swap
|
||||||
image: ghcr.io/mostlygeek/llama-swap:v199-vulkan-b8637
|
image: ghcr.io/mostlygeek/llama-swap:v199-vulkan-b8589
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /app/llama-swap
|
- /app/llama-swap
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: open-webui
|
chart: open-webui
|
||||||
version: 12.13.0
|
version: 13.0.1
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: HelmRepository
|
kind: HelmRepository
|
||||||
name: open-webui
|
name: open-webui
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ data:
|
|||||||
RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1
|
RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate@lumpiasty.xyz>
|
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate@lumpiasty.xyz>
|
||||||
|
RENOVATE_ALLOWED_COMMANDS: '["^node utils/update-garm-cli-hash\\.mjs$", "^node utils/update-garm-image-pin\\.mjs$"]'
|
||||||
|
|||||||
18
devenv.lock
18
devenv.lock
@@ -3,11 +3,11 @@
|
|||||||
"devenv": {
|
"devenv": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"dir": "src/modules",
|
"dir": "src/modules",
|
||||||
"lastModified": 1775201809,
|
"lastModified": 1773504385,
|
||||||
"narHash": "sha256-WmpoCegCQ6Q2ZyxqO05zlz/7XXjt/l2iut4Nk5Nt+W4=",
|
"narHash": "sha256-ANaeR+xVHxjGz36VI4qlZUbdhrlSE0xU7O7AUJKw3zU=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv",
|
"repo": "devenv",
|
||||||
"rev": "42a5505d4700e791732e48a38b4cca05a755f94b",
|
"rev": "4bce49e6f60c69e99eeb643efbbf74125cefd329",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
"treefmt-nix": "treefmt-nix"
|
"treefmt-nix": "treefmt-nix"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775175041,
|
"lastModified": 1773451905,
|
||||||
"narHash": "sha256-lYCPSMIV26VazREzl/TIpbWhBXJ+vJ0EJ+308TrX/6w=",
|
"narHash": "sha256-S/bukFEwbOYQbnR5UpciwYA42aEt1w5LK73GwARhsaA=",
|
||||||
"owner": "a1994sc",
|
"owner": "a1994sc",
|
||||||
"repo": "krew2nix",
|
"repo": "krew2nix",
|
||||||
"rev": "15c594042f1ba80ce97ab190a9c684a44c613590",
|
"rev": "bc779a8cf59ebf76ae60556bfe2d781a0a4cdbd9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -60,11 +60,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775036866,
|
"lastModified": 1773389992,
|
||||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
"narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
"rev": "c06b4ae3d6599a672a6210b7021d699c351eebda",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
15
devenv.nix
15
devenv.nix
@@ -6,6 +6,8 @@ let
|
|||||||
hvac
|
hvac
|
||||||
librouteros
|
librouteros
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
garm-cli = pkgs.callPackage ./nix/garm-cli.nix { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Overlays - apply krew2nix to get kubectl with krew support
|
# Overlays - apply krew2nix to get kubectl with krew support
|
||||||
@@ -41,18 +43,9 @@ in
|
|||||||
openbao
|
openbao
|
||||||
pv-migrate
|
pv-migrate
|
||||||
mermaid-cli
|
mermaid-cli
|
||||||
(
|
opencode
|
||||||
# Wrapping opencode to set the OPENCODE_ENABLE_EXA environment variable
|
garm-cli
|
||||||
runCommand "opencode" {
|
|
||||||
buildInputs = [ makeWrapper ];
|
|
||||||
} ''
|
|
||||||
mkdir -p $out/bin
|
|
||||||
makeWrapper ${pkgs.opencode}/bin/opencode $out/bin/opencode \
|
|
||||||
--set OPENCODE_ENABLE_EXA "1"
|
|
||||||
''
|
|
||||||
)
|
|
||||||
tea
|
tea
|
||||||
woodpecker-cli
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# Scripts
|
# Scripts
|
||||||
|
|||||||
28
docker/garm/Dockerfile
Normal file
28
docker/garm/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
FROM golang:1.26-alpine AS build
|
||||||
|
|
||||||
|
ARG GARM_COMMIT
|
||||||
|
ARG GARM_PROVIDER_K8S_VERSION=0.3.2
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates git wget tar build-base util-linux-dev linux-headers
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
RUN git clone https://github.com/cloudbase/garm.git . && git checkout "${GARM_COMMIT}"
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath \
|
||||||
|
-tags osusergo,netgo,sqlite_omit_load_extension \
|
||||||
|
-ldflags="-linkmode external -extldflags '-static' -s -w" \
|
||||||
|
-o /out/garm ./cmd/garm
|
||||||
|
|
||||||
|
RUN mkdir -p /out/providers.d \
|
||||||
|
&& wget -qO /tmp/garm-provider-k8s.tar.gz "https://github.com/mercedes-benz/garm-provider-k8s/releases/download/v${GARM_PROVIDER_K8S_VERSION}/garm-provider-k8s_Linux_x86_64.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/garm-provider-k8s.tar.gz -C /out/providers.d \
|
||||||
|
&& chmod 0755 /out/providers.d/garm-provider-k8s
|
||||||
|
|
||||||
|
FROM busybox
|
||||||
|
|
||||||
|
COPY --from=build /out/garm /bin/garm
|
||||||
|
COPY --from=build /out/providers.d/garm-provider-k8s /opt/garm/providers.d/garm-provider-k8s
|
||||||
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/garm"]
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Roles with needed access for OpenBao's Kubernetes secret engine
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRole
|
|
||||||
metadata:
|
|
||||||
name: k8s-full-secrets-abilities
|
|
||||||
rules:
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["namespaces"]
|
|
||||||
verbs: ["get"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["serviceaccounts", "serviceaccounts/token"]
|
|
||||||
verbs: ["create", "update", "delete"]
|
|
||||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
|
||||||
resources: ["rolebindings", "clusterrolebindings"]
|
|
||||||
verbs: ["create", "update", "delete"]
|
|
||||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
|
||||||
resources: ["roles", "clusterroles"]
|
|
||||||
verbs: ["bind", "escalate", "create", "update", "delete"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
metadata:
|
|
||||||
name: openbao-token-creator-binding
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: ClusterRole
|
|
||||||
name: k8s-full-secrets-abilities
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: openbao
|
|
||||||
namespace: openbao
|
|
||||||
@@ -25,4 +25,3 @@ resources:
|
|||||||
|
|
||||||
- configs/openbao-volume.yaml
|
- configs/openbao-volume.yaml
|
||||||
- controllers/openbao.yaml
|
- controllers/openbao.yaml
|
||||||
- configs/openbao-k8s-se-role.yaml
|
|
||||||
|
|||||||
45
nix/garm-cli.nix
Normal file
45
nix/garm-cli.nix
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{ lib, buildGoModule, fetchFromGitHub, installShellFiles }:
|
||||||
|
|
||||||
|
buildGoModule rec {
|
||||||
|
pname = "garm-cli";
|
||||||
|
version = "r1380";
|
||||||
|
garmCommit = "818a9dddccba5f2843f185e6a846770988f31fc5";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "cloudbase";
|
||||||
|
repo = "garm";
|
||||||
|
rev = garmCommit;
|
||||||
|
hash = "sha256-CTqqabNYUMSrmnQVCWml1/vkDw+OP1uJo1KFhBSZpYY=";
|
||||||
|
};
|
||||||
|
|
||||||
|
subPackages = [ "cmd/garm-cli" ];
|
||||||
|
|
||||||
|
nativeBuildInputs = [ installShellFiles ];
|
||||||
|
|
||||||
|
vendorHash = null;
|
||||||
|
|
||||||
|
ldflags = [
|
||||||
|
"-s"
|
||||||
|
"-w"
|
||||||
|
"-X main.version=${version}"
|
||||||
|
];
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
# We need to set a temporary HOME for the completion scripts as workaround
|
||||||
|
# because garm-cli tries to write config to the home directory
|
||||||
|
# when generating the completion scripts
|
||||||
|
export HOME="$(mktemp -d)"
|
||||||
|
|
||||||
|
installShellCompletion --cmd garm-cli \
|
||||||
|
--bash <($out/bin/garm-cli completion bash) \
|
||||||
|
--fish <($out/bin/garm-cli completion fish) \
|
||||||
|
--zsh <($out/bin/garm-cli completion zsh)
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "CLI for GitHub Actions Runner Manager";
|
||||||
|
homepage = "https://github.com/cloudbase/garm";
|
||||||
|
license = lib.licenses.asl20;
|
||||||
|
mainProgram = "garm-cli";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,8 +10,57 @@
|
|||||||
"gotk-components\\.ya?ml$"
|
"gotk-components\\.ya?ml$"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"customManagers": [
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track garm-cli pinned main commit",
|
||||||
|
"managerFilePatterns": ["^nix/garm-cli\\.nix$"],
|
||||||
|
"matchStrings": ["garmCommit = \\\"(?<currentValue>[a-f0-9]{40})\\\";"],
|
||||||
|
"depNameTemplate": "cloudbase/garm",
|
||||||
|
"datasourceTemplate": "github-refs",
|
||||||
|
"versioningTemplate": "git"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track garm-provider-k8s release in garm image Dockerfile",
|
||||||
|
"managerFilePatterns": ["^docker/garm/Dockerfile$"],
|
||||||
|
"matchStrings": ["ARG GARM_PROVIDER_K8S_VERSION=(?<currentValue>[0-9]+\\.[0-9]+\\.[0-9]+)"],
|
||||||
|
"depNameTemplate": "mercedes-benz/garm-provider-k8s",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track pinned garm main commit",
|
||||||
|
"managerFilePatterns": ["^apps/garm/image-source\\.env$"],
|
||||||
|
"matchStrings": ["GARM_COMMIT=(?<currentValue>[a-f0-9]{40})"],
|
||||||
|
"depNameTemplate": "cloudbase/garm",
|
||||||
|
"datasourceTemplate": "github-refs",
|
||||||
|
"versioningTemplate": "git"
|
||||||
|
}
|
||||||
|
],
|
||||||
"prHourlyLimit": 9,
|
"prHourlyLimit": 9,
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["custom.regex"],
|
||||||
|
"matchDepNames": ["cloudbase/garm"],
|
||||||
|
"matchFileNames": ["nix/garm-cli.nix"],
|
||||||
|
"postUpgradeTasks": {
|
||||||
|
"commands": ["node utils/update-garm-cli-hash.mjs"],
|
||||||
|
"fileFilters": ["nix/garm-cli.nix"],
|
||||||
|
"executionMode": "update"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["custom.regex"],
|
||||||
|
"matchDepNames": ["cloudbase/garm"],
|
||||||
|
"matchFileNames": ["apps/garm/image-source.env"],
|
||||||
|
"postUpgradeTasks": {
|
||||||
|
"commands": ["node utils/update-garm-image-pin.mjs"],
|
||||||
|
"fileFilters": ["apps/garm/image-source.env", "apps/garm/deployment.yaml"],
|
||||||
|
"executionMode": "update"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchDatasources": ["docker"],
|
"matchDatasources": ["docker"],
|
||||||
"matchPackageNames": ["ghcr.io/mostlygeek/llama-swap"],
|
"matchPackageNames": ["ghcr.io/mostlygeek/llama-swap"],
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import pathlib
|
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import hvac
|
import hvac
|
||||||
@@ -43,7 +42,7 @@ def synchronize_auth_kubernetes_config(client: hvac.Client):
|
|||||||
def synchronize_kubernetes_roles(client: hvac.Client):
|
def synchronize_kubernetes_roles(client: hvac.Client):
|
||||||
kubernetes = Kubernetes(client.adapter)
|
kubernetes = Kubernetes(client.adapter)
|
||||||
|
|
||||||
policy_dir = os.path.join(os.path.dirname(__file__), '../vault/kubernetes-auth-roles/')
|
policy_dir = os.path.join(os.path.dirname(__file__), '../vault/kubernetes-roles/')
|
||||||
|
|
||||||
roles: dict[str, Any] = {} # pyright:ignore[reportExplicitAny]
|
roles: dict[str, Any] = {} # pyright:ignore[reportExplicitAny]
|
||||||
for filename in os.listdir(policy_dir):
|
for filename in os.listdir(policy_dir):
|
||||||
@@ -68,69 +67,6 @@ def synchronize_kubernetes_roles(client: hvac.Client):
|
|||||||
# Using write data instead of kubernetes.create_role, we can pass raw yaml
|
# Using write data instead of kubernetes.create_role, we can pass raw yaml
|
||||||
_ = client.write_data(f'/auth/kubernetes/role/{role_name}', data=role_content) # pyright:ignore[reportAny]
|
_ = client.write_data(f'/auth/kubernetes/role/{role_name}', data=role_content) # pyright:ignore[reportAny]
|
||||||
|
|
||||||
def synchronize_approle_auth(client: hvac.Client):
|
|
||||||
if client.sys.list_auth_methods().get('approle/') is None:
|
|
||||||
print('Enabling AppRole auth method')
|
|
||||||
client.sys.enable_auth_method('approle', 'AppRole authorization for CI')
|
|
||||||
|
|
||||||
roles_dir = pathlib.Path(__file__).parent.joinpath('../vault/approles/')
|
|
||||||
roles: dict[str, Any] = {}
|
|
||||||
|
|
||||||
for filename in roles_dir.iterdir():
|
|
||||||
with filename.open('r') as f:
|
|
||||||
role = yaml.safe_load(f.read())
|
|
||||||
assert type(role) is dict
|
|
||||||
roles[filename.stem] = role
|
|
||||||
|
|
||||||
roles_on_vault: list[str] = []
|
|
||||||
roles_response = client.list("auth/approle/roles")
|
|
||||||
if roles_response is not None:
|
|
||||||
roles_on_vault = roles_response['data']['keys']
|
|
||||||
|
|
||||||
for role in roles_on_vault:
|
|
||||||
if role not in roles:
|
|
||||||
print(f'Deleting role: {role}')
|
|
||||||
client.delete(f'auth/approle/role/{role}')
|
|
||||||
|
|
||||||
for role_name, role_content in roles.items():
|
|
||||||
print(f'Updating role: {role_name}')
|
|
||||||
client.write_data(f'auth/approle/role/{role_name}', data=role_content)
|
|
||||||
|
|
||||||
def synchronize_kubernetes_secretengine(client: hvac.Client):
|
|
||||||
# Ensure kubernetes secret engine is enabled
|
|
||||||
if client.sys.list_mounted_secrets_engines().get('kubernetes/') is None:
|
|
||||||
print('Enabling kubernetes secret engine')
|
|
||||||
client.sys.enable_secrets_engine('kubernetes', 'kubernetes', 'Cluster access')
|
|
||||||
|
|
||||||
# Write empty config (all defaults, working on the same cluster)
|
|
||||||
client.write('kubernetes/config', None)
|
|
||||||
|
|
||||||
policy_dir = pathlib.Path(__file__).parent.joinpath('../vault/kubernetes-se-roles/')
|
|
||||||
roles: dict[str, Any] = {}
|
|
||||||
|
|
||||||
for filename in policy_dir.iterdir():
|
|
||||||
with filename.open('r') as f:
|
|
||||||
role = yaml.safe_load(f.read())
|
|
||||||
assert type(role) is dict
|
|
||||||
# generated_role_rules must be json or yaml formatted string, convert it
|
|
||||||
if 'generated_role_rules' in role and type(role['generated_role_rules']) is not str:
|
|
||||||
role['generated_role_rules'] = yaml.safe_dump(role['generated_role_rules'])
|
|
||||||
roles[filename.stem] = role
|
|
||||||
|
|
||||||
roles_on_vault: list[str] = []
|
|
||||||
roles_response = client.list("kubernetes/roles")
|
|
||||||
if roles_response is not None:
|
|
||||||
roles_on_vault = roles_response['data']['keys']
|
|
||||||
|
|
||||||
for role in roles_on_vault:
|
|
||||||
if role not in roles:
|
|
||||||
print(f'Deleting role: {role}')
|
|
||||||
client.delete(f'kubernetes/roles/{role}')
|
|
||||||
|
|
||||||
for role_name, role_content in roles.items():
|
|
||||||
print(f'Updating role: {role_name}')
|
|
||||||
client.write_data(f'kubernetes/roles/{role_name}', data=role_content)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="synchronizeVault",
|
prog="synchronizeVault",
|
||||||
@@ -146,11 +82,5 @@ if __name__ == '__main__':
|
|||||||
print('Synchronizing kubernetes config')
|
print('Synchronizing kubernetes config')
|
||||||
synchronize_auth_kubernetes_config(client)
|
synchronize_auth_kubernetes_config(client)
|
||||||
|
|
||||||
print('Synchronizing kubernetes auth roles')
|
print('Synchronizing kubernetes roles')
|
||||||
synchronize_kubernetes_roles(client)
|
synchronize_kubernetes_roles(client)
|
||||||
|
|
||||||
print('Synchronizing AppRole auth method')
|
|
||||||
synchronize_approle_auth(client)
|
|
||||||
|
|
||||||
print('Synchronizing kubernetes secret engine')
|
|
||||||
synchronize_kubernetes_secretengine(client)
|
|
||||||
|
|||||||
320
utils/update-garm-cli-hash.mjs
Normal file
320
utils/update-garm-cli-hash.mjs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import https from "node:https";
|
||||||
|
import zlib from "node:zlib";
|
||||||
|
|
||||||
|
const nixFile = "nix/garm-cli.nix";
|
||||||
|
|
||||||
|
function die(message) {
|
||||||
|
console.error(message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(filePath) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(filePath, "utf8");
|
||||||
|
} catch {
|
||||||
|
die(`Missing ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVersion(text) {
|
||||||
|
const match = text.match(/^\s*version\s*=\s*"([^"]+)";/m);
|
||||||
|
if (!match) {
|
||||||
|
die(`Unable to extract version from ${nixFile}`);
|
||||||
|
}
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCommit(text) {
|
||||||
|
const match = text.match(/^\s*garmCommit\s*=\s*"([a-f0-9]{40})";/m);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeU64LE(hash, value) {
|
||||||
|
const buf = Buffer.alloc(8);
|
||||||
|
buf.writeBigUInt64LE(BigInt(value), 0);
|
||||||
|
hash.update(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeNarString(hash, data) {
|
||||||
|
writeU64LE(hash, data.length);
|
||||||
|
hash.update(data);
|
||||||
|
const pad = (8 - (data.length % 8)) % 8;
|
||||||
|
if (pad) {
|
||||||
|
hash.update(Buffer.alloc(pad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeNarText(hash, text) {
|
||||||
|
writeNarString(hash, Buffer.from(text, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOctal(field) {
|
||||||
|
const clean = field.toString("ascii").replace(/\0.*$/, "").trim();
|
||||||
|
if (!clean) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number.parseInt(clean, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTarHeader(block) {
|
||||||
|
const name = block.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
||||||
|
const mode = parseOctal(block.subarray(100, 108));
|
||||||
|
const size = parseOctal(block.subarray(124, 136));
|
||||||
|
const typeflagRaw = block[156];
|
||||||
|
const typeflag = typeflagRaw === 0 ? "0" : String.fromCharCode(typeflagRaw);
|
||||||
|
const linkname = block.subarray(157, 257).toString("utf8").replace(/\0.*$/, "");
|
||||||
|
const prefix = block.subarray(345, 500).toString("utf8").replace(/\0.*$/, "");
|
||||||
|
return {
|
||||||
|
name: prefix ? `${prefix}/${name}` : name,
|
||||||
|
mode,
|
||||||
|
size,
|
||||||
|
typeflag,
|
||||||
|
linkname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePax(data) {
|
||||||
|
const out = {};
|
||||||
|
let i = 0;
|
||||||
|
while (i < data.length) {
|
||||||
|
let sp = i;
|
||||||
|
while (sp < data.length && data[sp] !== 0x20) sp += 1;
|
||||||
|
if (sp >= data.length) break;
|
||||||
|
const len = Number.parseInt(data.subarray(i, sp).toString("utf8"), 10);
|
||||||
|
if (!Number.isFinite(len) || len <= 0) break;
|
||||||
|
const record = data.subarray(sp + 1, i + len).toString("utf8");
|
||||||
|
const eq = record.indexOf("=");
|
||||||
|
if (eq > 0) {
|
||||||
|
const key = record.slice(0, eq);
|
||||||
|
const value = record.slice(eq + 1).replace(/\n$/, "");
|
||||||
|
out[key] = value;
|
||||||
|
}
|
||||||
|
i += len;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTarEntries(archiveBuffer) {
|
||||||
|
const gz = zlib.gunzipSync(archiveBuffer);
|
||||||
|
const entries = [];
|
||||||
|
let i = 0;
|
||||||
|
let pendingPax = null;
|
||||||
|
let longName = null;
|
||||||
|
let longLink = null;
|
||||||
|
|
||||||
|
while (i + 512 <= gz.length) {
|
||||||
|
const header = gz.subarray(i, i + 512);
|
||||||
|
i += 512;
|
||||||
|
|
||||||
|
if (header.every((b) => b === 0)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = parseTarHeader(header);
|
||||||
|
const data = gz.subarray(i, i + h.size);
|
||||||
|
const dataPad = (512 - (h.size % 512)) % 512;
|
||||||
|
i += h.size + dataPad;
|
||||||
|
|
||||||
|
if (h.typeflag === "x") {
|
||||||
|
pendingPax = parsePax(data);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (h.typeflag === "g") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (h.typeflag === "L") {
|
||||||
|
longName = data.toString("utf8").replace(/\0.*$/, "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (h.typeflag === "K") {
|
||||||
|
longLink = data.toString("utf8").replace(/\0.*$/, "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = pendingPax?.path ?? longName ?? h.name;
|
||||||
|
const linkpath = pendingPax?.linkpath ?? longLink ?? h.linkname;
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
path,
|
||||||
|
typeflag: h.typeflag,
|
||||||
|
mode: h.mode,
|
||||||
|
linkname: linkpath,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingPax = null;
|
||||||
|
longName = null;
|
||||||
|
longLink = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripTopDir(path) {
|
||||||
|
const cleaned = path.replace(/^\.?\//, "").replace(/\/$/, "");
|
||||||
|
const idx = cleaned.indexOf("/");
|
||||||
|
if (idx === -1) return "";
|
||||||
|
return cleaned.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(root, relPath) {
|
||||||
|
if (!relPath) return root;
|
||||||
|
const parts = relPath.split("/").filter(Boolean);
|
||||||
|
let cur = root;
|
||||||
|
for (const part of parts) {
|
||||||
|
let child = cur.children.get(part);
|
||||||
|
if (!child) {
|
||||||
|
child = { kind: "directory", children: new Map() };
|
||||||
|
cur.children.set(part, child);
|
||||||
|
}
|
||||||
|
if (child.kind !== "directory") {
|
||||||
|
die(`Path conflict while building tree at ${relPath}`);
|
||||||
|
}
|
||||||
|
cur = child;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTree(entries) {
|
||||||
|
const root = { kind: "directory", children: new Map() };
|
||||||
|
for (const entry of entries) {
|
||||||
|
const rel = stripTopDir(entry.path);
|
||||||
|
if (!rel) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = rel.split("/").filter(Boolean);
|
||||||
|
const name = parts.pop();
|
||||||
|
const parent = ensureDir(root, parts.join("/"));
|
||||||
|
|
||||||
|
if (entry.typeflag === "5") {
|
||||||
|
const existing = parent.children.get(name);
|
||||||
|
if (!existing) {
|
||||||
|
parent.children.set(name, { kind: "directory", children: new Map() });
|
||||||
|
} else if (existing.kind !== "directory") {
|
||||||
|
die(`Path conflict at ${rel}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.typeflag === "2") {
|
||||||
|
parent.children.set(name, { kind: "symlink", target: entry.linkname });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.typeflag === "0") {
|
||||||
|
parent.children.set(name, {
|
||||||
|
kind: "regular",
|
||||||
|
executable: (entry.mode & 0o111) !== 0,
|
||||||
|
contents: Buffer.from(entry.data),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareUtf8(a, b) {
|
||||||
|
return Buffer.from(a, "utf8").compare(Buffer.from(b, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function narDump(hash, node) {
|
||||||
|
if (node.kind === "directory") {
|
||||||
|
writeNarText(hash, "(");
|
||||||
|
writeNarText(hash, "type");
|
||||||
|
writeNarText(hash, "directory");
|
||||||
|
const names = [...node.children.keys()].sort(compareUtf8);
|
||||||
|
for (const name of names) {
|
||||||
|
writeNarText(hash, "entry");
|
||||||
|
writeNarText(hash, "(");
|
||||||
|
writeNarText(hash, "name");
|
||||||
|
writeNarString(hash, Buffer.from(name, "utf8"));
|
||||||
|
writeNarText(hash, "node");
|
||||||
|
narDump(hash, node.children.get(name));
|
||||||
|
writeNarText(hash, ")");
|
||||||
|
}
|
||||||
|
writeNarText(hash, ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "symlink") {
|
||||||
|
writeNarText(hash, "(");
|
||||||
|
writeNarText(hash, "type");
|
||||||
|
writeNarText(hash, "symlink");
|
||||||
|
writeNarText(hash, "target");
|
||||||
|
writeNarString(hash, Buffer.from(node.target, "utf8"));
|
||||||
|
writeNarText(hash, ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNarText(hash, "(");
|
||||||
|
writeNarText(hash, "type");
|
||||||
|
writeNarText(hash, "regular");
|
||||||
|
if (node.executable) {
|
||||||
|
writeNarText(hash, "executable");
|
||||||
|
writeNarText(hash, "");
|
||||||
|
}
|
||||||
|
writeNarText(hash, "contents");
|
||||||
|
writeNarString(hash, node.contents);
|
||||||
|
writeNarText(hash, ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchBuffer(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https
|
||||||
|
.get(url, (res) => {
|
||||||
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
const redirectUrl = new URL(res.headers.location, url).toString();
|
||||||
|
res.resume();
|
||||||
|
fetchBuffer(redirectUrl).then(resolve, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
||||||
|
reject(new Error(`Failed to fetch ${url}: ${res.statusCode ?? "unknown"}`));
|
||||||
|
res.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunks = [];
|
||||||
|
res.on("data", (chunk) => chunks.push(chunk));
|
||||||
|
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
|
})
|
||||||
|
.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSRIFromGitHubTar(ref) {
|
||||||
|
const url = `https://github.com/cloudbase/garm/archive/${ref}.tar.gz`;
|
||||||
|
return fetchBuffer(url).then((archive) => {
|
||||||
|
const entries = parseTarEntries(archive);
|
||||||
|
const root = buildTree(entries);
|
||||||
|
const hash = createHash("sha256");
|
||||||
|
writeNarText(hash, "nix-archive-1");
|
||||||
|
narDump(hash, root);
|
||||||
|
return `sha256-${hash.digest("base64")}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHash(text, sri) {
|
||||||
|
const pattern = /(^\s*hash\s*=\s*")sha256-[^"]+(";)/m;
|
||||||
|
if (!pattern.test(text)) {
|
||||||
|
die(`Unable to update hash in ${nixFile}`);
|
||||||
|
}
|
||||||
|
const next = text.replace(pattern, `$1${sri}$2`);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const text = readText(nixFile);
|
||||||
|
const version = extractVersion(text);
|
||||||
|
const commit = extractCommit(text);
|
||||||
|
const ref = commit ?? `v${version}`;
|
||||||
|
const sri = await computeSRIFromGitHubTar(ref);
|
||||||
|
const updated = updateHash(text, sri);
|
||||||
|
fs.writeFileSync(nixFile, updated, "utf8");
|
||||||
|
console.log(`Updated ${nixFile} hash to ${sri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => die(err.message));
|
||||||
91
utils/update-garm-image-pin.mjs
Normal file
91
utils/update-garm-image-pin.mjs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
const pinFile = "apps/garm/image-source.env";
|
||||||
|
const deploymentFile = "apps/garm/deployment.yaml";
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnvFile(content) {
|
||||||
|
const vars = {};
|
||||||
|
for (const line of content.split(/\r?\n/)) {
|
||||||
|
if (!line || line.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const idx = line.indexOf("=");
|
||||||
|
if (idx === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = line.slice(0, idx).trim();
|
||||||
|
const value = line.slice(idx + 1).trim();
|
||||||
|
vars[key] = value;
|
||||||
|
}
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOrAdd(content, key, value) {
|
||||||
|
const pattern = new RegExp(`^${key}=.*$`, "m");
|
||||||
|
if (pattern.test(content)) {
|
||||||
|
return content.replace(pattern, `${key}=${value}`);
|
||||||
|
}
|
||||||
|
return `${content.trimEnd()}\n${key}=${value}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitOut(args, options = {}) {
|
||||||
|
return execFileSync("git", args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
...options,
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitRun(args, options = {}) {
|
||||||
|
execFileSync("git", args, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinContent = fs.readFileSync(pinFile, "utf8");
|
||||||
|
const vars = parseEnvFile(pinContent);
|
||||||
|
const commit = vars.GARM_COMMIT;
|
||||||
|
const imageRepo = vars.GARM_IMAGE_REPO || "gitea.lumpiasty.xyz/lumpiasty/garm-k8s";
|
||||||
|
|
||||||
|
if (!commit || !/^[0-9a-f]{40}$/.test(commit)) {
|
||||||
|
fail(`Invalid or missing GARM_COMMIT in ${pinFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "garm-main-"));
|
||||||
|
let commitNumber;
|
||||||
|
try {
|
||||||
|
gitRun(["clone", "--filter=blob:none", "https://github.com/cloudbase/garm.git", tmpDir], {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
commitNumber = gitOut(["-C", tmpDir, "rev-list", "--count", commit]);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(commitNumber)) {
|
||||||
|
fail(`Unable to resolve commit number for ${commit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = `${imageRepo}:r${commitNumber}`;
|
||||||
|
|
||||||
|
let nextPin = pinContent;
|
||||||
|
nextPin = updateOrAdd(nextPin, "GARM_COMMIT_NUMBER", commitNumber);
|
||||||
|
nextPin = updateOrAdd(nextPin, "GARM_IMAGE_REPO", imageRepo);
|
||||||
|
nextPin = updateOrAdd(nextPin, "GARM_IMAGE", image);
|
||||||
|
fs.writeFileSync(pinFile, nextPin, "utf8");
|
||||||
|
|
||||||
|
const deployment = fs.readFileSync(deploymentFile, "utf8");
|
||||||
|
const imagePattern = /image:\s*(?:ghcr\.io\/cloudbase\/garm:[^\s]+|gitea\.lumpiasty\.xyz\/(?:Lumpiasty|lumpiasty)\/garm(?:-k8s)?:[^\s]+)/;
|
||||||
|
if (!imagePattern.test(deployment)) {
|
||||||
|
fail(`Unable to update garm image in ${deploymentFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDeployment = deployment.replace(imagePattern, `image: ${image}`);
|
||||||
|
|
||||||
|
fs.writeFileSync(deploymentFile, updatedDeployment, "utf8");
|
||||||
|
console.log(`Pinned garm image to ${image}`);
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
token_ttl: 20m
|
|
||||||
token_max_ttl: 20m
|
|
||||||
policies:
|
|
||||||
- flux-reconcile
|
|
||||||
6
vault/kubernetes-roles/garm.yaml
Normal file
6
vault/kubernetes-roles/garm.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
bound_service_account_names:
|
||||||
|
- garm
|
||||||
|
bound_service_account_namespaces:
|
||||||
|
- garm
|
||||||
|
token_policies:
|
||||||
|
- garm
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
allowed_kubernetes_namespaces: flux-system
|
|
||||||
generated_role_rules:
|
|
||||||
rules:
|
|
||||||
- apiGroups: ["kustomize.toolkit.fluxcd.io"]
|
|
||||||
resources: ["gitrepositories"]
|
|
||||||
verbs: ["update", "watch"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
path "kubernetes/creds/flux-reconcile" {
|
|
||||||
capabilities = ["update"]
|
|
||||||
}
|
|
||||||
7
vault/policy/garm.hcl
Normal file
7
vault/policy/garm.hcl
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
path "secret/data/garm" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "secret/data/backblaze" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user