diff --git a/kubernetes/apps/youtrack/deployment.yaml b/kubernetes/apps/youtrack/deployment.yaml new file mode 100644 index 00000000..1bed88fa --- /dev/null +++ b/kubernetes/apps/youtrack/deployment.yaml @@ -0,0 +1,90 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: youtrack + namespace: youtrack + labels: + app.kubernetes.io/name: youtrack +spec: + replicas: 1 + # One process owns the database directory. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: youtrack + template: + metadata: + labels: + app.kubernetes.io/name: youtrack + spec: + automountServiceAccountToken: false + enableServiceLinks: false + securityContext: + runAsNonRoot: true + # The image's service account. + runAsUser: 13001 + runAsGroup: 13001 + fsGroup: 13001 + seccompProfile: + type: RuntimeDefault + containers: + - name: youtrack + image: jetbrains/youtrack:2026.2.19728@sha256:c5e9cedc6fb1d7bcc9b3b56fc3f63e563623e1cd39396161eb70957991583950 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + ports: + - name: http + containerPort: 8080 + resources: + requests: + cpu: 250m + memory: 2Gi + limits: + memory: 4Gi + startupProbe: + tcpSocket: + port: http + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + tcpSocket: + port: http + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + periodSeconds: 30 + failureThreshold: 4 + volumeMounts: + - name: youtrack + mountPath: /opt/youtrack/data + subPath: data + - name: youtrack + mountPath: /opt/youtrack/conf + subPath: conf + - name: youtrack + mountPath: /opt/youtrack/logs + subPath: logs + - name: youtrack + mountPath: /opt/youtrack/backups + subPath: backups + volumes: + - name: youtrack + persistentVolumeClaim: + claimName: youtrack +--- +apiVersion: v1 +kind: Service +metadata: + name: youtrack + namespace: youtrack +spec: + selector: + app.kubernetes.io/name: youtrack + ports: + - name: http + port: 8080 + targetPort: http diff --git a/kubernetes/apps/youtrack/ingress.yaml b/kubernetes/apps/youtrack/ingress.yaml new file mode 100644 index 00000000..e88b6c9c --- /dev/null +++ b/kubernetes/apps/youtrack/ingress.yaml @@ -0,0 +1,27 @@ +# LAN-only (UDM record via dns.renner.uno/internal); no public label until login goes through Authentik. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: youtrack + namespace: youtrack + labels: + dns.renner.uno/internal: "true" + annotations: + cert-manager.io/cluster-issuer: letsencrypt-cluster +spec: + ingressClassName: traefik + tls: + - hosts: + - youtrack.renner.uno + secretName: youtrack-tls + rules: + - host: youtrack.renner.uno + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: youtrack + port: + number: 8080 diff --git a/kubernetes/apps/youtrack/kustomization.yaml b/kubernetes/apps/youtrack/kustomization.yaml new file mode 100644 index 00000000..ad23ae39 --- /dev/null +++ b/kubernetes/apps/youtrack/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - pvc.yaml + - deployment.yaml + - ingress.yaml + - oidcclient.yaml diff --git a/kubernetes/apps/youtrack/oidcclient.yaml b/kubernetes/apps/youtrack/oidcclient.yaml new file mode 100644 index 00000000..b3160f03 --- /dev/null +++ b/kubernetes/apps/youtrack/oidcclient.yaml @@ -0,0 +1,13 @@ +# Authentik OIDC client, created by authentik-gate-sync; issuer https://auth.renner.uno/application/o/youtrack/. +apiVersion: auth.renner.uno/v1alpha1 +kind: OIDCClient +metadata: + name: youtrack + namespace: youtrack +spec: + launchURL: https://youtrack.renner.uno + group: YouTrack Users + redirectURIs: + # YouTrack generates the callback when the auth module is created; pin it strict once known. + - matchingMode: regex + url: ^https://youtrack\.renner\.uno/.*$ diff --git a/kubernetes/apps/youtrack/pvc.yaml b/kubernetes/apps/youtrack/pvc.yaml new file mode 100644 index 00000000..101908f4 --- /dev/null +++ b/kubernetes/apps/youtrack/pvc.yaml @@ -0,0 +1,17 @@ +# YouTrack data, conf, logs and backups (subPaths); Velero youtrack-daily. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: youtrack + namespace: youtrack + labels: + app.kubernetes.io/name: youtrack + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled +spec: + accessModes: + - ReadWriteOnce + storageClassName: ceph-block + resources: + requests: + storage: 20Gi diff --git a/kubernetes/clusters/homelab/restore.yaml b/kubernetes/clusters/homelab/restore.yaml index 33f6d1ee..a44acc80 100644 --- a/kubernetes/clusters/homelab/restore.yaml +++ b/kubernetes/clusters/homelab/restore.yaml @@ -394,3 +394,31 @@ spec: status.validationErrors.exists(e, e.startsWith('No backups found for schedule'))))) current: >- has(status.phase) && (status.phase == 'Completed' || status.phase == 'FailedValidation') +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: restore-youtrack + namespace: flux-system +spec: + interval: 10m + timeout: 1h + sourceRef: + kind: GitRepository + name: flux-system + path: ./kubernetes/restore/youtrack + prune: true + wait: true + dependsOn: + - name: restore-gitea + - name: velero-backups-synced + - name: infrastructure-ceph-cluster + healthCheckExprs: + - apiVersion: velero.io/v1 + kind: Restore + failed: >- + has(status.phase) && (status.phase in ['Failed', 'PartiallyFailed'] || + (status.phase == 'FailedValidation' && !(has(status.validationErrors) && + status.validationErrors.exists(e, e.startsWith('No backups found for schedule'))))) + current: >- + has(status.phase) && (status.phase == 'Completed' || status.phase == 'FailedValidation') diff --git a/kubernetes/clusters/homelab/youtrack.yaml b/kubernetes/clusters/homelab/youtrack.yaml new file mode 100644 index 00000000..cc11ca1d --- /dev/null +++ b/kubernetes/clusters/homelab/youtrack.yaml @@ -0,0 +1,25 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: youtrack + namespace: flux-system +spec: + interval: 10m + retryInterval: 1m + timeout: 15m + sourceRef: + kind: GitRepository + name: flux-system + path: ./kubernetes/apps/youtrack + prune: true + wait: true + # OIDCClient Ready tracks Authentik (e.g. the group exists), not this apply. + healthCheckExprs: + - apiVersion: auth.renner.uno/v1alpha1 + kind: OIDCClient + current: "true" + dependsOn: + - name: restore-youtrack + - name: infrastructure + - name: infrastructure-crds + - name: infrastructure-ceph-cluster diff --git a/kubernetes/infrastructure/namespaces/namespaces.yaml b/kubernetes/infrastructure/namespaces/namespaces.yaml index 1a22a6ef..05ba3f3a 100644 --- a/kubernetes/infrastructure/namespaces/namespaces.yaml +++ b/kubernetes/infrastructure/namespaces/namespaces.yaml @@ -83,6 +83,11 @@ metadata: --- apiVersion: v1 kind: Namespace +metadata: + name: youtrack +--- +apiVersion: v1 +kind: Namespace metadata: name: immich --- diff --git a/kubernetes/infrastructure/network-policies/kustomization.yaml b/kubernetes/infrastructure/network-policies/kustomization.yaml index ea5327db..3d27cff2 100644 --- a/kubernetes/infrastructure/network-policies/kustomization.yaml +++ b/kubernetes/infrastructure/network-policies/kustomization.yaml @@ -32,6 +32,7 @@ resources: - wordpress-ns.yaml - ai-ns.yaml - navidrome-ns.yaml + - youtrack-ns.yaml - uptime-monitor-ns.yaml - kuma-sync-ns.yaml - cloudflare-lb-sync-ns.yaml diff --git a/kubernetes/infrastructure/network-policies/youtrack-ns.yaml b/kubernetes/infrastructure/network-policies/youtrack-ns.yaml new file mode 100644 index 00000000..1b2cc7a2 --- /dev/null +++ b/kubernetes/infrastructure/network-policies/youtrack-ns.yaml @@ -0,0 +1,107 @@ +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: default-deny-ingress + namespace: youtrack +spec: + endpointSelector: {} + ingress: + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": youtrack + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": ingress + app.kubernetes.io/name: traefik + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": monitoring + - fromEntities: + - host + - remote-node + # ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See + # home-assistant-ns.yaml for the rationale (CT-RELATED path not + # passing related ICMP empirically; per-namespace shape, mirroring + # commit 3c517a0's verified-safe pattern with the 66dbe9f type set). + - fromEndpoints: + - matchExpressions: + - key: io.kubernetes.pod.namespace + operator: Exists + icmps: + - fields: + - { type: 0, family: IPv4 } + - { type: 3, family: IPv4 } + - { type: 8, family: IPv4 } + - { type: 11, family: IPv4 } + - { type: 12, family: IPv4 } + - { type: 1, family: IPv6 } + - { type: 2, family: IPv6 } + - { type: 3, family: IPv6 } + - { type: 4, family: IPv6 } + - { type: 128, family: IPv6 } + - { type: 129, family: IPv6 } + - { type: 133, family: IPv6 } + - { type: 134, family: IPv6 } + - { type: 135, family: IPv6 } + - { type: 136, family: IPv6 } + - { type: 143, family: IPv6 } + egress: + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": youtrack + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": ingress + app.kubernetes.io/name: traefik + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + # host/remote-node: kubelet probes + node-local reach (Ceph mons on host net) + - toEntities: + - host + - remote-node + # ICMP — egress mirror; see ingress block above. + - toEndpoints: + - matchExpressions: + - key: io.kubernetes.pod.namespace + operator: Exists + icmps: + - fields: + - { type: 0, family: IPv4 } + - { type: 3, family: IPv4 } + - { type: 8, family: IPv4 } + - { type: 11, family: IPv4 } + - { type: 12, family: IPv4 } + - { type: 1, family: IPv6 } + - { type: 2, family: IPv6 } + - { type: 3, family: IPv6 } + - { type: 4, family: IPv6 } + - { type: 128, family: IPv6 } + - { type: 129, family: IPv6 } + - { type: 133, family: IPv6 } + - { type: 134, family: IPv6 } + - { type: 135, family: IPv6 } + - { type: 136, family: IPv6 } + - { type: 143, family: IPv6 } +--- +# Outbound HTTPS: OIDC discovery/token/JWKS at auth.renner.uno (Traefik VIP, matched as traefik +# pods above) and JetBrains services (license, update check), mail-free. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: youtrack-egress-https + namespace: youtrack +spec: + endpointSelector: {} + egress: + - toEntities: + - world + toPorts: + - ports: + - port: "443" + protocol: TCP diff --git a/kubernetes/infrastructure/velero-schedules/schedules.yaml b/kubernetes/infrastructure/velero-schedules/schedules.yaml index ccea8bc6..e024113f 100644 --- a/kubernetes/infrastructure/velero-schedules/schedules.yaml +++ b/kubernetes/infrastructure/velero-schedules/schedules.yaml @@ -369,3 +369,23 @@ spec: labelSelector: matchLabels: app.kubernetes.io/name: synapse +--- +apiVersion: velero.io/v1 +kind: Schedule +metadata: + name: youtrack-daily + namespace: velero + labels: + app.kubernetes.io/part-of: homelab-backup +spec: + # Daily at 10:15 local, next free stagger slot after matrix-daily at 09:45. + schedule: "15 10 * * *" + useOwnerReferencesInBackup: false + template: + includedNamespaces: + - youtrack + includedResources: + - persistentvolumeclaims + storageLocation: default + ttl: 720h0m0s + snapshotMoveData: true diff --git a/kubernetes/restore/youtrack/kustomization.yaml b/kubernetes/restore/youtrack/kustomization.yaml new file mode 100644 index 00000000..bc08b2d5 --- /dev/null +++ b/kubernetes/restore/youtrack/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - restore.yaml diff --git a/kubernetes/restore/youtrack/restore.yaml b/kubernetes/restore/youtrack/restore.yaml new file mode 100644 index 00000000..9a409297 --- /dev/null +++ b/kubernetes/restore/youtrack/restore.yaml @@ -0,0 +1,7 @@ +apiVersion: velero.io/v1 +kind: Restore +metadata: + name: youtrack + namespace: velero +spec: + scheduleName: youtrack-daily