diff --git a/charts/gohoarder/Chart.yaml b/charts/gohoarder/Chart.yaml new file mode 100644 index 0000000..0372bd3 --- /dev/null +++ b/charts/gohoarder/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: gohoarder +description: A universal package cache proxy supporting npm, PyPI, and Go modules with security scanning +type: application +version: 0.0.1 +appVersion: "0.0.1" +keywords: + - package-manager + - cache + - proxy + - npm + - pypi + - go-modules + - security + - vulnerability-scanning +home: https://github.com/lukaszraczylo/gohoarder +sources: + - https://github.com/lukaszraczylo/gohoarder +maintainers: + - name: Lukasz Raczylo + email: lukasz@raczylo.com +icon: https://raw.githubusercontent.com/lukaszraczylo/gohoarder/main/docs/logo.png diff --git a/charts/gohoarder/LICENSE b/charts/gohoarder/LICENSE new file mode 100644 index 0000000..745270f --- /dev/null +++ b/charts/gohoarder/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Lukasz Raczylo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/charts/gohoarder/README.md b/charts/gohoarder/README.md new file mode 100644 index 0000000..c41e26b --- /dev/null +++ b/charts/gohoarder/README.md @@ -0,0 +1,499 @@ +# GoHoarder Helm Chart + +A universal package cache proxy supporting npm, PyPI, and Go modules with integrated security scanning. + +## Features + +- **Multi-Registry Support**: Proxy for npm, PyPI, and Go modules +- **Security Scanning**: Integrated vulnerability scanning with multiple scanners +- **Flexible Storage**: Support for filesystem, S3, and SMB storage backends +- **Metadata Storage**: SQLite or PostgreSQL for metadata +- **Auto-Configuration**: Generates configuration from Helm values +- **Production Ready**: Includes health checks, resource limits, and security contexts + +## Prerequisites + +- Kubernetes 1.19+ +- Helm 3.0+ +- PV provisioner support in the underlying infrastructure (for persistent storage) + +## Installation + +### Add Helm Repository + +```bash +helm repo add gohoarder https://lukaszraczylo.github.io/gohoarder +helm repo update +``` + +### Install Chart + +```bash +# Install with default values +helm install gohoarder gohoarder/gohoarder + +# Install with custom values +helm install gohoarder gohoarder/gohoarder -f values.yaml + +# Install in a specific namespace +helm install gohoarder gohoarder/gohoarder -n gohoarder --create-namespace +``` + +## Quick Start Examples + +### Minimal Installation + +```bash +helm install gohoarder gohoarder/gohoarder \ + --set global.domain=example.com \ + --set ingress.enabled=true +``` + +### With Security Scanning + +```bash +helm install gohoarder gohoarder/gohoarder \ + --set security.enabled=true \ + --set security.scanners.trivy.enabled=true \ + --set security.scanners.osv.enabled=true +``` + +### With S3 Storage + +```bash +helm install gohoarder gohoarder/gohoarder \ + --set storage.backend=s3 \ + --set storage.s3.bucket=my-bucket \ + --set storage.s3.region=us-east-1 \ + --set storage.s3.accessKeyId=AKIAIOSFODNN7EXAMPLE \ + --set storage.s3.secretAccessKey=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +``` + +### With Private Container Registry + +If using images from a private registry, create an image pull secret and reference it: + +```bash +# Create a Docker registry secret +kubectl create secret docker-registry ghcr-secret \ + --docker-server=ghcr.io \ + --docker-username= \ + --docker-password= \ + --docker-email= \ + -n gohoarder + +# Install with the secret +helm install gohoarder gohoarder/gohoarder \ + --set global.imagePullSecrets[0].name=ghcr-secret \ + -n gohoarder +``` + +Or using a values file to reference existing secrets: + +```yaml +global: + imagePullSecrets: + - name: ghcr-secret + - name: dockerhub-secret # Multiple secrets supported +``` + +**Auto-create secrets** (chart will create them for you): + +```yaml +imageCredentials: + ghcr-secret: + registry: ghcr.io + username: myusername + password: mytoken + email: myemail@example.com + +global: + imagePullSecrets: + - name: ghcr-secret +``` + +> **Note**: Storing credentials in values files is less secure than creating secrets manually. Consider using external secret management solutions like Sealed Secrets or External Secrets Operator for production. + +## Configuration Methods + +GoHoarder supports two configuration methods that can be used together: + +### 1. ConfigMap (Default) + +The chart automatically generates a `config.yaml` from Helm values and mounts it as a ConfigMap. This is the default approach and works out of the box. + +### 2. Environment Variables + +You can override any configuration using environment variables with the format `GOHOARDER_` where dots are replaced with underscores. + +**Example using values file:** + +```yaml +server: + env: + - name: GOHOARDER_STORAGE_BACKEND + value: "s3" + - name: GOHOARDER_STORAGE_S3_BUCKET + value: "my-bucket" + # Reference secrets for sensitive data + - name: GOHOARDER_STORAGE_S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: aws-credentials + key: secret-access-key + - name: GOHOARDER_METADATA_POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: password +``` + +**Example using command line:** + +```bash +helm install gohoarder gohoarder/gohoarder \ + --set server.env[0].name=GOHOARDER_STORAGE_BACKEND \ + --set server.env[0].value=s3 \ + --set server.env[1].name=GOHOARDER_LOGGING_LEVEL \ + --set server.env[1].value=debug +``` + +**Benefits of environment variables:** +- Better integration with Kubernetes secrets +- Override specific values without modifying ConfigMap +- Support for secret references (no plain-text passwords) +- Compatible with external secret management (External Secrets Operator, Sealed Secrets) + +**Common environment variable mappings:** + +| Config Path | Environment Variable | +|-------------|---------------------| +| `storage.backend` | `GOHOARDER_STORAGE_BACKEND` | +| `storage.s3.bucket` | `GOHOARDER_STORAGE_S3_BUCKET` | +| `storage.s3.region` | `GOHOARDER_STORAGE_S3_REGION` | +| `storage.s3.access_key_id` | `GOHOARDER_STORAGE_S3_ACCESS_KEY_ID` | +| `storage.s3.secret_access_key` | `GOHOARDER_STORAGE_S3_SECRET_ACCESS_KEY` | +| `metadata.backend` | `GOHOARDER_METADATA_BACKEND` | +| `metadata.postgresql.host` | `GOHOARDER_METADATA_POSTGRESQL_HOST` | +| `metadata.postgresql.password` | `GOHOARDER_METADATA_POSTGRESQL_PASSWORD` | +| `security.enabled` | `GOHOARDER_SECURITY_ENABLED` | +| `security.scanners.trivy.enabled` | `GOHOARDER_SECURITY_SCANNERS_TRIVY_ENABLED` | +| `logging.level` | `GOHOARDER_LOGGING_LEVEL` | +| `logging.format` | `GOHOARDER_LOGGING_FORMAT` | + +## Configuration Reference + +The following table lists the configurable parameters and their default values. + +### Global Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nameOverride` | Override the name of the chart | `""` | +| `fullnameOverride` | Override the full name of the chart | `""` | +| `global.domain` | Base domain for the deployment | `gohoarder.local` | +| `global.imagePullSecrets` | Image pull secrets (reference existing) | `[]` | +| `imageCredentials` | Auto-create image pull secrets from credentials | `{}` | + +### Replica Count + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount.server` | Number of server replicas | `1` | +| `replicaCount.frontend` | Number of frontend replicas | `1` | +| `replicaCount.scanner` | Number of scanner replicas | `1` | + +### Image Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.server.repository` | Server image repository | `ghcr.io/lukaszraczylo/gohoarder-server` | +| `image.server.tag` | Server image tag | `latest` | +| `image.server.pullPolicy` | Server image pull policy | `IfNotPresent` | +| `image.frontend.repository` | Frontend image repository | `ghcr.io/lukaszraczylo/gohoarder-frontend` | +| `image.frontend.tag` | Frontend image tag | `latest` | +| `image.frontend.pullPolicy` | Frontend image pull policy | `IfNotPresent` | +| `image.scanner.repository` | Scanner image repository | `ghcr.io/lukaszraczylo/gohoarder-scanner` | +| `image.scanner.tag` | Scanner image tag | `latest` | +| `image.scanner.pullPolicy` | Scanner image pull policy | `IfNotPresent` | + +### Environment Variables + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `server.env` | Additional environment variables for server | `[]` | +| `frontend.env` | Additional environment variables for frontend | `[]` | +| `scanner.env` | Additional environment variables for scanner | `[]` | + +### Storage Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `storage.backend` | Storage backend (filesystem, s3, smb) | `filesystem` | +| `storage.filesystem.storageClass` | Storage class for PVC | `""` | +| `storage.filesystem.size` | Storage size | `100Gi` | +| `storage.filesystem.useHostPath` | Use hostPath instead of PVC | `false` | +| `storage.filesystem.hostPath` | Host path for storage | `/var/lib/gohoarder` | +| `storage.s3.endpoint` | S3 endpoint | `s3.amazonaws.com` | +| `storage.s3.bucket` | S3 bucket name | `gohoarder-cache` | +| `storage.s3.region` | S3 region | `us-east-1` | + +### Metadata Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `metadata.backend` | Metadata backend (sqlite, postgresql) | `sqlite` | +| `metadata.sqlite.persistence.enabled` | Enable persistence for SQLite | `true` | +| `metadata.sqlite.persistence.size` | SQLite storage size | `10Gi` | +| `metadata.postgresql.host` | PostgreSQL host | `localhost` | +| `metadata.postgresql.database` | PostgreSQL database | `gohoarder` | + +### Security Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `security.enabled` | Enable security scanning | `false` | +| `security.blockOnSeverity` | Block packages on severity | `high` | +| `security.scanners.trivy.enabled` | Enable Trivy scanner | `false` | +| `security.scanners.osv.enabled` | Enable OSV scanner | `false` | +| `security.scanners.grype.enabled` | Enable Grype scanner | `false` | + +### Authentication + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `auth.enabled` | Enable authentication | `true` | +| `auth.adminApiKey` | Admin API key (auto-generated if empty) | `""` | +| `auth.existingSecret` | Use existing secret for admin key | `""` | + +### Ingress + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `ingress.enabled` | Enable ingress | `false` | +| `ingress.className` | Ingress class name | `nginx` | +| `ingress.frontend.enabled` | Enable frontend ingress | `true` | +| `ingress.frontend.host` | Frontend hostname | `gohoarder.local` | +| `ingress.frontend.tls.enabled` | Enable TLS for frontend | `false` | + +## High Availability & Scaling + +### Running Multiple Server Replicas + +GoHoarder can run with multiple server replicas for high availability and load distribution, but the configuration must be set correctly to avoid data inconsistency. + +#### ✅ Compatible Configurations (Safe for Multiple Replicas) + +**Storage:** +- ✅ **S3** - Fully compatible, recommended for production HA setups +- ✅ **SMB** - Compatible, shared network storage +- ✅ **Filesystem with RWX** - Compatible when using ReadWriteMany storage classes + - ✅ Examples: Longhorn RWX, NFS, CephFS, GlusterFS, Azure Files + - ✅ Uses atomic rename operations for safe concurrent writes + - ✅ Packages are static/immutable - perfect for shared storage + - ❌ Not compatible with local storage or ReadWriteOnce (RWO) PVCs + +**Metadata:** +- ✅ **PostgreSQL** - Fully compatible, handles concurrent writes, recommended for HA +- ⚠️ **SQLite** - Limited compatibility: + - Uses WAL mode which supports concurrent reads + - Multiple writers can cause lock contention + - Works but may have performance issues under high concurrency + - Only if using shared storage (NFS, etc.) + +#### 📋 Recommended HA Configurations + +**Option 1: Cloud Storage (S3)** + +Best for cloud deployments, object storage: + +```yaml +replicaCount: + server: 3 + +storage: + backend: s3 + s3: + endpoint: s3.amazonaws.com + region: us-east-1 + bucket: gohoarder-cache + +metadata: + backend: postgresql + postgresql: + host: postgres.database.svc.cluster.local + database: gohoarder + +podDisruptionBudget: + enabled: true + minAvailable: 1 +``` + +**Option 2: Shared Filesystem (Longhorn/NFS)** + +Best for on-premises or self-hosted Kubernetes: + +```yaml +replicaCount: + server: 3 + +storage: + backend: filesystem + filesystem: + # Use RWX storage class (Longhorn, NFS, CephFS, etc.) + storageClass: "longhorn" # or "nfs-client", "cephfs", etc. + size: "500Gi" + accessMode: "ReadWriteMany" # RWX - Critical for multiple replicas! + +metadata: + backend: postgresql # Or SQLite with RWX storage + postgresql: + host: postgres.database.svc.cluster.local + database: gohoarder + +podDisruptionBudget: + enabled: true + minAvailable: 1 +``` + +**Why Filesystem with RWX Works:** +- Packages are immutable once cached (static files) +- Filesystem backend uses atomic `rename()` operations +- Race condition safe: If two replicas cache same package, one wins +- Performance: Local filesystem often faster than object storage for reads + +#### ⚠️ What Won't Work with Multiple Replicas + +**Filesystem storage with local volumes:** +```yaml +# ❌ DON'T DO THIS with multiple replicas +storage: + backend: filesystem + filesystem: + useHostPath: true # Each replica gets different storage +``` + +**SQLite with local storage:** +```yaml +# ⚠️ AVOID with multiple replicas +metadata: + backend: sqlite + sqlite: + persistence: + enabled: true # Each replica gets its own database +``` + +#### 🔄 How It Works + +**Request Deduplication:** +- Single replica: Uses `singleflight` to prevent duplicate upstream fetches +- Multiple replicas: Each replica may fetch the same package independently +- **Mitigation**: Package metadata in shared database prevents duplicate downloads once one replica completes + +**Cache Consistency:** +- Storage backend (S3/SMB) ensures all replicas see the same cached packages +- Metadata database ensures consistent package information across replicas +- First replica to cache a package wins, others will use the cached version + +**Session Affinity:** +- Not required - GoHoarder is stateless +- Load balancer can distribute requests randomly + +**Scanner Replicas:** +- Scanner can run as a single replica or multiple +- If multiple scanners enabled, they share work through the metadata database +- Package scans are deduplicated via database state + +#### 🔬 Technical Details: Concurrent Write Safety + +**Filesystem Backend with RWX Storage:** + +The filesystem storage backend uses a **temp-file + atomic rename** pattern: + +```go +1. Write package to: /cache/npm/package@1.0.0.tmp +2. Calculate checksums (MD5, SHA256) +3. Atomic rename: .tmp → /cache/npm/package@1.0.0 +``` + +**Why this is safe for concurrent writes:** +- `os.Rename()` is atomic on POSIX filesystems +- If two replicas cache the same package simultaneously: + - Both write to separate `.tmp` files + - Both attempt atomic rename + - One succeeds, one gets "file exists" error + - Result: Same file content, no corruption + +**Package immutability:** +- Packages are versioned and immutable (npm/pypi/go semantics) +- Same package@version always has identical content +- Concurrent writes produce identical results +- No risk of partial/corrupted files + +**Quota tracking:** +- Per-process mutex (minor inaccuracy across replicas) +- Conservative: May undercount slightly +- Not critical for operation + +## Uninstallation + +```bash +helm uninstall gohoarder -n gohoarder +``` + +## Upgrading + +```bash +helm upgrade gohoarder gohoarder/gohoarder -f values.yaml +``` + +## Package Manager Configuration + +After installation, configure your package managers to use GoHoarder: + +### NPM + +```bash +npm config set registry http:///npm/ +``` + +### Go + +```bash +export GOPROXY=http:///go,direct +``` + +### PyPI + +```bash +pip config set global.index-url http:///pypi/simple +``` + +## Troubleshooting + +### Check Pod Status + +```bash +kubectl get pods -n gohoarder +kubectl logs -n gohoarder +``` + +### Verify Configuration + +```bash +kubectl get configmap -n gohoarder -gohoarder-config -o yaml +``` + +### Get Admin API Key + +```bash +kubectl get secret -n gohoarder -gohoarder-auth -o jsonpath='{.data.admin-api-key}' | base64 -d +``` + +## Contributing + +Contributions are welcome! Please visit [GitHub](https://github.com/lukaszraczylo/gohoarder) for more information. + +## License + +See the [LICENSE](https://github.com/lukaszraczylo/gohoarder/blob/main/LICENSE) file. diff --git a/charts/gohoarder/templates/NOTES.txt b/charts/gohoarder/templates/NOTES.txt new file mode 100644 index 0000000..15f6d50 --- /dev/null +++ b/charts/gohoarder/templates/NOTES.txt @@ -0,0 +1,70 @@ +** GoHoarder has been installed! ** + +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- if .Values.ingress.frontend.enabled }} + http{{ if .Values.ingress.frontend.tls.enabled }}s{{ end }}://{{ .Values.ingress.frontend.host | default (printf "%s.%s" "gohoarder" .Values.global.domain) }} +{{- end }} +{{- else if contains "NodePort" .Values.frontend.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "gohoarder.fullname" . }}-frontend) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.frontend.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "gohoarder.fullname" . }}-frontend' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "gohoarder.fullname" . }}-frontend --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.frontend.service.port }} +{{- else if contains "ClusterIP" .Values.frontend.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "gohoarder.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=frontend" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} + +2. Admin API Key: +{{- if .Values.auth.enabled }} +{{- if .Values.auth.existingSecret }} + The admin API key is stored in the existing secret: {{ .Values.auth.existingSecret }} + + To retrieve it: + kubectl get secret {{ .Values.auth.existingSecret }} -n {{ .Release.Namespace }} -o jsonpath='{.data.{{ .Values.auth.secretKey }}}' | base64 -d +{{- else if .Values.auth.adminApiKey }} + The admin API key you provided: {{ .Values.auth.adminApiKey }} +{{- else }} + A random admin API key has been generated. To retrieve it: + kubectl get secret {{ include "gohoarder.fullname" . }}-auth -n {{ .Release.Namespace }} -o jsonpath='{.data.{{ .Values.auth.secretKey }}}' | base64 -d +{{- end }} +{{- else }} + Authentication is disabled. +{{- end }} + +3. Configuration: + - Storage backend: {{ .Values.storage.backend }} + - Metadata backend: {{ .Values.metadata.backend }} + - Security scanning: {{ if .Values.security.enabled }}enabled{{ else }}disabled{{ end }} + {{- if .Values.security.enabled }} + - Active scanners: + {{- range $scanner, $config := .Values.security.scanners }} + {{- if $config.enabled }} + * {{ $scanner }} + {{- end }} + {{- end }} + {{- end }} + +4. Package Proxies: + Configure your package managers to use GoHoarder: + + NPM: + npm config set registry http://{{ include "gohoarder.fullname" . }}-server.{{ .Release.Namespace }}.svc.cluster.local/npm/ + + Go: + export GOPROXY=http://{{ include "gohoarder.fullname" . }}-server.{{ .Release.Namespace }}.svc.cluster.local/go,direct + + PyPI: + pip config set global.index-url http://{{ include "gohoarder.fullname" . }}-server.{{ .Release.Namespace }}.svc.cluster.local/pypi/simple + +5. Health Checks: + - Server health: http://{{ include "gohoarder.fullname" . }}-server.{{ .Release.Namespace }}.svc.cluster.local/health + - Server ready: http://{{ include "gohoarder.fullname" . }}-server.{{ .Release.Namespace }}.svc.cluster.local/health/ready + +For more information, visit: https://github.com/lukaszraczylo/gohoarder diff --git a/charts/gohoarder/templates/_helpers.tpl b/charts/gohoarder/templates/_helpers.tpl new file mode 100644 index 0000000..b3232b5 --- /dev/null +++ b/charts/gohoarder/templates/_helpers.tpl @@ -0,0 +1,174 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "gohoarder.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "gohoarder.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "gohoarder.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "gohoarder.labels" -}} +helm.sh/chart: {{ include "gohoarder.chart" . }} +{{ include "gohoarder.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "gohoarder.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gohoarder.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Server labels +*/}} +{{- define "gohoarder.server.labels" -}} +{{ include "gohoarder.labels" . }} +app.kubernetes.io/component: server +{{- end }} + +{{/* +Server selector labels +*/}} +{{- define "gohoarder.server.selectorLabels" -}} +{{ include "gohoarder.selectorLabels" . }} +app.kubernetes.io/component: server +{{- end }} + +{{/* +Frontend labels +*/}} +{{- define "gohoarder.frontend.labels" -}} +{{ include "gohoarder.labels" . }} +app.kubernetes.io/component: frontend +{{- end }} + +{{/* +Frontend selector labels +*/}} +{{- define "gohoarder.frontend.selectorLabels" -}} +{{ include "gohoarder.selectorLabels" . }} +app.kubernetes.io/component: frontend +{{- end }} + +{{/* +Scanner labels +*/}} +{{- define "gohoarder.scanner.labels" -}} +{{ include "gohoarder.labels" . }} +app.kubernetes.io/component: scanner +{{- end }} + +{{/* +Scanner selector labels +*/}} +{{- define "gohoarder.scanner.selectorLabels" -}} +{{ include "gohoarder.selectorLabels" . }} +app.kubernetes.io/component: scanner +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "gohoarder.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "gohoarder.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Generate admin API key +*/}} +{{- define "gohoarder.adminApiKey" -}} +{{- if .Values.auth.adminApiKey }} +{{- .Values.auth.adminApiKey }} +{{- else }} +{{- randAlphaNum 32 }} +{{- end }} +{{- end }} + +{{/* +Storage volume configuration +*/}} +{{- define "gohoarder.storageVolume" -}} +{{- if eq .Values.storage.backend "filesystem" }} +{{- if .Values.storage.filesystem.useHostPath }} +- name: storage + hostPath: + path: {{ .Values.storage.filesystem.hostPath }} + type: DirectoryOrCreate +{{- else if .Values.storage.filesystem.existingClaim }} +- name: storage + persistentVolumeClaim: + claimName: {{ .Values.storage.filesystem.existingClaim }} +{{- else }} +- name: storage + persistentVolumeClaim: + claimName: {{ include "gohoarder.fullname" . }}-storage +{{- end }} +{{- else }} +- name: storage + emptyDir: {} +{{- end }} +{{- end }} + +{{/* +Metadata volume configuration +*/}} +{{- define "gohoarder.metadataVolume" -}} +{{- if and (eq .Values.metadata.backend "sqlite") .Values.metadata.sqlite.persistence.enabled }} +{{- if .Values.metadata.sqlite.persistence.existingClaim }} +- name: metadata + persistentVolumeClaim: + claimName: {{ .Values.metadata.sqlite.persistence.existingClaim }} +{{- else }} +- name: metadata + persistentVolumeClaim: + claimName: {{ include "gohoarder.fullname" . }}-metadata +{{- end }} +{{- else }} +- name: metadata + emptyDir: {} +{{- end }} +{{- end }} + +{{/* +Trivy cache volume configuration +*/}} +{{- define "gohoarder.trivyCacheVolume" -}} +{{- if .Values.security.scanners.trivy.enabled }} +- name: trivy-cache + emptyDir: {} +{{- end }} +{{- end }} diff --git a/charts/gohoarder/templates/configmap.yaml b/charts/gohoarder/templates/configmap.yaml new file mode 100644 index 0000000..cc742a4 --- /dev/null +++ b/charts/gohoarder/templates/configmap.yaml @@ -0,0 +1,168 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gohoarder.fullname" . }}-config + labels: + {{- include "gohoarder.labels" . | nindent 4 }} +data: + config.yaml: | + server: + host: {{ .Values.server.host | quote }} + port: {{ .Values.server.port }} + read_timeout: {{ .Values.server.readTimeout | quote }} + write_timeout: {{ .Values.server.writeTimeout | quote }} + idle_timeout: {{ .Values.server.idleTimeout | quote }} + tls: + enabled: false + + storage: + backend: {{ .Values.storage.backend | quote }} + {{- if eq .Values.storage.backend "filesystem" }} + path: "/var/cache/gohoarder" + filesystem: + base_path: "/var/cache/gohoarder" + {{- else if eq .Values.storage.backend "s3" }} + s3: + endpoint: {{ .Values.storage.s3.endpoint | quote }} + region: {{ .Values.storage.s3.region | quote }} + bucket: {{ .Values.storage.s3.bucket | quote }} + {{- if .Values.storage.s3.existingSecret }} + access_key_id: "${S3_ACCESS_KEY_ID}" + secret_access_key: "${S3_SECRET_ACCESS_KEY}" + {{- else }} + access_key_id: {{ .Values.storage.s3.accessKeyId | quote }} + secret_access_key: {{ .Values.storage.s3.secretAccessKey | quote }} + {{- end }} + use_ssl: {{ .Values.storage.s3.useSSL }} + {{- else if eq .Values.storage.backend "smb" }} + smb: + host: {{ .Values.storage.smb.host | quote }} + share: {{ .Values.storage.smb.share | quote }} + {{- if .Values.storage.smb.existingSecret }} + username: "${SMB_USERNAME}" + password: "${SMB_PASSWORD}" + {{- else }} + username: {{ .Values.storage.smb.username | quote }} + password: {{ .Values.storage.smb.password | quote }} + {{- end }} + domain: {{ .Values.storage.smb.domain | quote }} + {{- end }} + + metadata: + backend: {{ .Values.metadata.backend | quote }} + {{- if eq .Values.metadata.backend "sqlite" }} + connection: "file:/var/lib/gohoarder/metadata/gohoarder.db?cache=shared&mode=rwc" + sqlite: + path: "/var/lib/gohoarder/metadata/gohoarder.db" + wal_mode: {{ .Values.metadata.sqlite.walMode }} + {{- else if eq .Values.metadata.backend "postgresql" }} + postgresql: + host: {{ .Values.metadata.postgresql.host | quote }} + port: {{ .Values.metadata.postgresql.port }} + database: {{ .Values.metadata.postgresql.database | quote }} + {{- if .Values.metadata.postgresql.existingSecret }} + user: "${POSTGRES_USER}" + password: "${POSTGRES_PASSWORD}" + {{- else }} + user: {{ .Values.metadata.postgresql.username | quote }} + password: {{ .Values.metadata.postgresql.password | quote }} + {{- end }} + ssl_mode: {{ .Values.metadata.postgresql.sslMode | quote }} + {{- end }} + + cache: + default_ttl: {{ .Values.cache.defaultTTL | quote }} + cleanup_interval: {{ .Values.cache.cleanupInterval | quote }} + max_size_bytes: {{ .Values.cache.maxSizeBytes }} + per_project_quota: {{ .Values.cache.perProjectQuota }} + ttl_overrides: + {{- range $key, $value := .Values.cache.ttlOverrides }} + {{ $key }}: {{ $value | quote }} + {{- end }} + + security: + enabled: {{ .Values.security.enabled }} + block_on_severity: {{ .Values.security.blockOnSeverity | quote }} + scan_on_download: {{ .Values.security.scanOnDownload }} + rescan_interval: {{ .Values.security.rescanInterval | quote }} + update_db_on_startup: {{ .Values.security.updateDbOnStartup }} + block_thresholds: + critical: {{ .Values.security.blockThresholds.critical }} + high: {{ .Values.security.blockThresholds.high }} + medium: {{ .Values.security.blockThresholds.medium }} + low: {{ .Values.security.blockThresholds.low }} + scanners: + trivy: + enabled: {{ .Values.security.scanners.trivy.enabled }} + timeout: {{ .Values.security.scanners.trivy.timeout | quote }} + cache_db: {{ .Values.security.scanners.trivy.cacheDb | quote }} + osv: + enabled: {{ .Values.security.scanners.osv.enabled }} + api_url: {{ .Values.security.scanners.osv.apiUrl | quote }} + timeout: {{ .Values.security.scanners.osv.timeout | quote }} + grype: + enabled: {{ .Values.security.scanners.grype.enabled }} + timeout: {{ .Values.security.scanners.grype.timeout | quote }} + govulncheck: + enabled: {{ .Values.security.scanners.govulncheck.enabled }} + timeout: {{ .Values.security.scanners.govulncheck.timeout | quote }} + npm_audit: + enabled: {{ .Values.security.scanners.npmAudit.enabled }} + timeout: {{ .Values.security.scanners.npmAudit.timeout | quote }} + pip_audit: + enabled: {{ .Values.security.scanners.pipAudit.enabled }} + timeout: {{ .Values.security.scanners.pipAudit.timeout | quote }} + ghsa: + enabled: {{ .Values.security.scanners.ghsa.enabled }} + timeout: {{ .Values.security.scanners.ghsa.timeout | quote }} + {{- if or .Values.security.scanners.ghsa.token .Values.security.scanners.ghsa.existingSecret }} + token: "${GHSA_TOKEN}" + {{- end }} + static: + enabled: {{ .Values.security.scanners.static.enabled }} + max_package_size: {{ .Values.security.scanners.static.maxPackageSize }} + check_checksums: {{ .Values.security.scanners.static.checkChecksums }} + block_suspicious: {{ .Values.security.scanners.static.blockSuspicious }} + + auth: + enabled: {{ .Values.auth.enabled }} + key_expiration: {{ .Values.auth.keyExpiration | quote }} + bcrypt_cost: {{ .Values.auth.bcryptCost }} + audit_log: {{ .Values.auth.auditLog }} + + network: + connect_timeout: {{ .Values.network.connectTimeout | quote }} + read_timeout: {{ .Values.network.readTimeout | quote }} + write_timeout: {{ .Values.network.writeTimeout | quote }} + max_idle_conns: {{ .Values.network.maxIdleConns }} + max_conns_per_host: {{ .Values.network.maxConnsPerHost }} + rate_limit: + per_api_key: {{ .Values.network.rateLimit.perApiKey }} + per_ip: {{ .Values.network.rateLimit.perIp }} + burst_size: {{ .Values.network.rateLimit.burstSize }} + circuit_breaker: + threshold: {{ .Values.network.circuitBreaker.threshold }} + timeout: {{ .Values.network.circuitBreaker.timeout | quote }} + reset_interval: {{ .Values.network.circuitBreaker.resetInterval | quote }} + retry: + max_attempts: {{ .Values.network.retry.maxAttempts }} + initial_backoff: {{ .Values.network.retry.initialBackoff | quote }} + max_backoff: {{ .Values.network.retry.maxBackoff | quote }} + + logging: + level: {{ .Values.logging.level | quote }} + format: {{ .Values.logging.format | quote }} + + handlers: + go: + enabled: {{ .Values.handlers.go.enabled }} + upstream_proxy: {{ .Values.handlers.go.upstreamProxy | quote }} + checksum_db: {{ .Values.handlers.go.checksumDb | quote }} + verify_checksums: {{ .Values.handlers.go.verifyChecksums }} + npm: + enabled: {{ .Values.handlers.npm.enabled }} + upstream_registry: {{ .Values.handlers.npm.upstreamRegistry | quote }} + pypi: + enabled: {{ .Values.handlers.pypi.enabled }} + upstream_url: {{ .Values.handlers.pypi.upstreamUrl | quote }} + simple_api_url: {{ .Values.handlers.pypi.simpleApiUrl | quote }} diff --git a/charts/gohoarder/templates/deployment-frontend.yaml b/charts/gohoarder/templates/deployment-frontend.yaml new file mode 100644 index 0000000..5d91921 --- /dev/null +++ b/charts/gohoarder/templates/deployment-frontend.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gohoarder.fullname" . }}-frontend + labels: + {{- include "gohoarder.frontend.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.frontend }} + {{- end }} + selector: + matchLabels: + {{- include "gohoarder.frontend.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gohoarder.frontend.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "gohoarder.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: frontend + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + image: "{{ .Values.image.frontend.repository }}:{{ .Values.image.frontend.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.frontend.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + env: + - name: API_BASE_URL + value: {{ .Values.frontend.backendUrl | default (printf "http://%s-server:%d" (include "gohoarder.fullname" .) (.Values.server.service.port | int)) | quote }} + - name: APP_VERSION + value: {{ .Chart.AppVersion | quote }} + - name: APP_NAME + value: "GoHoarder" + {{- with .Values.frontend.env }} + {{- toYaml . | nindent 8 }} + {{- end }} + livenessProbe: + {{- toYaml .Values.frontend.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.frontend.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-run + mountPath: /var/run + volumes: + - name: tmp + emptyDir: {} + - name: nginx-cache + emptyDir: {} + - name: nginx-run + emptyDir: {} + {{- with .Values.frontend.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.frontend.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.frontend.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/gohoarder/templates/deployment-scanner.yaml b/charts/gohoarder/templates/deployment-scanner.yaml new file mode 100644 index 0000000..61e8287 --- /dev/null +++ b/charts/gohoarder/templates/deployment-scanner.yaml @@ -0,0 +1,114 @@ +{{- if .Values.security.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gohoarder.fullname" . }}-scanner + labels: + {{- include "gohoarder.scanner.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount.scanner }} + selector: + matchLabels: + {{- include "gohoarder.scanner.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gohoarder.scanner.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "gohoarder.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: init-permissions + image: busybox:latest + command: ['sh', '-c'] + args: + - | + mkdir -p /var/cache/gohoarder /var/lib/gohoarder/metadata /tmp/gohoarder + {{- if .Values.security.scanners.trivy.enabled }} + mkdir -p {{ .Values.security.scanners.trivy.cacheDb }} + chown -R 1000:1000 {{ .Values.security.scanners.trivy.cacheDb }} + {{- end }} + chown -R 1000:1000 /var/cache/gohoarder /var/lib/gohoarder /tmp/gohoarder + chmod 750 /var/cache/gohoarder /var/lib/gohoarder + volumeMounts: + {{- include "gohoarder.storageVolume" . | nindent 8 }} + {{- include "gohoarder.metadataVolume" . | nindent 8 }} + {{- include "gohoarder.trivyCacheVolume" . | nindent 8 }} + - name: tmp + mountPath: /tmp/gohoarder + securityContext: + runAsUser: 0 + containers: + - name: scanner + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.scanner.repository }}:{{ .Values.image.scanner.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.scanner.pullPolicy }} + env: + - name: CONFIG_FILE + value: /etc/gohoarder/config.yaml + {{- if and .Values.security.scanners.ghsa.enabled .Values.security.scanners.ghsa.existingSecret }} + - name: GHSA_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.security.scanners.ghsa.existingSecret }} + key: token + {{- else if and .Values.security.scanners.ghsa.enabled .Values.security.scanners.ghsa.token }} + - name: GHSA_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-ghsa + key: token + {{- end }} + {{- with .Values.scanner.env }} + {{- toYaml . | nindent 8 }} + {{- end }} + resources: + {{- toYaml .Values.scanner.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/gohoarder + readOnly: true + - name: storage + mountPath: /var/cache/gohoarder + - name: metadata + mountPath: /var/lib/gohoarder/metadata + {{- if .Values.security.scanners.trivy.enabled }} + - name: trivy-cache + mountPath: {{ .Values.security.scanners.trivy.cacheDb }} + {{- end }} + - name: tmp + mountPath: /tmp + volumes: + - name: config + configMap: + name: {{ include "gohoarder.fullname" . }}-config + {{- include "gohoarder.storageVolume" . | nindent 6 }} + {{- include "gohoarder.metadataVolume" . | nindent 6 }} + {{- include "gohoarder.trivyCacheVolume" . | nindent 6 }} + - name: tmp + emptyDir: {} + {{- with .Values.scanner.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.scanner.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.scanner.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/gohoarder/templates/deployment-server.yaml b/charts/gohoarder/templates/deployment-server.yaml new file mode 100644 index 0000000..bdb90e5 --- /dev/null +++ b/charts/gohoarder/templates/deployment-server.yaml @@ -0,0 +1,194 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gohoarder.fullname" . }}-server + labels: + {{- include "gohoarder.server.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.server }} + {{- end }} + selector: + matchLabels: + {{- include "gohoarder.server.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gohoarder.server.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "gohoarder.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: init-permissions + image: busybox:latest + command: ['sh', '-c'] + args: + - | + mkdir -p /var/cache/gohoarder /var/lib/gohoarder/metadata /tmp/gohoarder + chown -R 1000:1000 /var/cache/gohoarder /var/lib/gohoarder /tmp/gohoarder + chmod 750 /var/cache/gohoarder /var/lib/gohoarder + volumeMounts: + {{- include "gohoarder.storageVolume" . | nindent 8 }} + {{- include "gohoarder.metadataVolume" . | nindent 8 }} + - name: tmp + mountPath: /tmp/gohoarder + securityContext: + runAsUser: 0 + containers: + - name: server + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.server.repository }}:{{ .Values.image.server.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.server.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.server.port }} + protocol: TCP + env: + - name: CONFIG_FILE + value: /etc/gohoarder/config.yaml + {{- if and .Values.auth.enabled .Values.auth.existingSecret }} + - name: ADMIN_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.auth.existingSecret }} + key: {{ .Values.auth.secretKey }} + {{- else if .Values.auth.enabled }} + - name: ADMIN_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-auth + key: {{ .Values.auth.secretKey }} + {{- end }} + {{- if and (eq .Values.storage.backend "s3") .Values.storage.s3.existingSecret }} + - name: S3_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.storage.s3.existingSecret }} + key: access-key-id + - name: S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.storage.s3.existingSecret }} + key: secret-access-key + {{- else if and (eq .Values.storage.backend "s3") .Values.storage.s3.accessKeyId }} + - name: S3_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-s3 + key: access-key-id + - name: S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-s3 + key: secret-access-key + {{- end }} + {{- if and (eq .Values.storage.backend "smb") .Values.storage.smb.existingSecret }} + - name: SMB_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.storage.smb.existingSecret }} + key: username + - name: SMB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.storage.smb.existingSecret }} + key: password + {{- else if and (eq .Values.storage.backend "smb") .Values.storage.smb.username }} + - name: SMB_USERNAME + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-smb + key: username + - name: SMB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-smb + key: password + {{- end }} + {{- if and (eq .Values.metadata.backend "postgresql") .Values.metadata.postgresql.existingSecret }} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ .Values.metadata.postgresql.existingSecret }} + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.metadata.postgresql.existingSecret }} + key: password + {{- else if and (eq .Values.metadata.backend "postgresql") .Values.metadata.postgresql.username }} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-postgresql + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-postgresql + key: password + {{- end }} + {{- if and .Values.security.scanners.ghsa.enabled .Values.security.scanners.ghsa.existingSecret }} + - name: GHSA_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.security.scanners.ghsa.existingSecret }} + key: token + {{- else if and .Values.security.scanners.ghsa.enabled .Values.security.scanners.ghsa.token }} + - name: GHSA_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "gohoarder.fullname" . }}-ghsa + key: token + {{- end }} + {{- with .Values.server.env }} + {{- toYaml . | nindent 8 }} + {{- end }} + livenessProbe: + {{- toYaml .Values.server.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.server.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.server.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/gohoarder + readOnly: true + - name: storage + mountPath: /var/cache/gohoarder + - name: metadata + mountPath: /var/lib/gohoarder/metadata + - name: tmp + mountPath: /tmp + volumes: + - name: config + configMap: + name: {{ include "gohoarder.fullname" . }}-config + {{- include "gohoarder.storageVolume" . | nindent 6 }} + {{- include "gohoarder.metadataVolume" . | nindent 6 }} + - name: tmp + emptyDir: {} + {{- with .Values.server.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/gohoarder/templates/imagepullsecret.yaml b/charts/gohoarder/templates/imagepullsecret.yaml new file mode 100644 index 0000000..6e92009 --- /dev/null +++ b/charts/gohoarder/templates/imagepullsecret.yaml @@ -0,0 +1,14 @@ +{{- if .Values.imageCredentials }} +{{- range $name, $config := .Values.imageCredentials }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "gohoarder.labels" $ | nindent 4 }} +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ printf "{\"auths\":{\"%s\":{\"username\":\"%s\",\"password\":\"%s\",\"email\":\"%s\",\"auth\":\"%s\"}}}" $config.registry $config.username $config.password $config.email (printf "%s:%s" $config.username $config.password | b64enc) | b64enc }} +{{- end }} +{{- end }} diff --git a/charts/gohoarder/templates/ingress.yaml b/charts/gohoarder/templates/ingress.yaml new file mode 100644 index 0000000..cd4f08c --- /dev/null +++ b/charts/gohoarder/templates/ingress.yaml @@ -0,0 +1,118 @@ +{{- if .Values.ingress.enabled -}} +{{- if .Values.ingress.frontend.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "gohoarder.fullname" . }}-frontend + labels: + {{- include "gohoarder.frontend.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.frontend.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.frontend.host | default (printf "%s.%s" "gohoarder" .Values.global.domain) | quote }} + secretName: {{ .Values.ingress.frontend.tls.secretName }} + {{- end }} + rules: + - host: {{ .Values.ingress.frontend.host | default (printf "%s.%s" "gohoarder" .Values.global.domain) | quote }} + http: + paths: + - path: /npm + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: /pypi + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: /go + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: /api + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: /ws + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: /health + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: /metrics + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} + - path: / + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-frontend + port: + number: {{ .Values.frontend.service.port }} +{{- end }} +--- +{{- if .Values.ingress.api.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "gohoarder.fullname" . }}-api + labels: + {{- include "gohoarder.server.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.api.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.api.host | default (printf "api.%s.%s" "gohoarder" .Values.global.domain) | quote }} + secretName: {{ .Values.ingress.api.tls.secretName }} + {{- end }} + rules: + - host: {{ .Values.ingress.api.host | default (printf "api.%s.%s" "gohoarder" .Values.global.domain) | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "gohoarder.fullname" . }}-server + port: + number: {{ .Values.server.service.port }} +{{- end }} +{{- end }} diff --git a/charts/gohoarder/templates/pvc.yaml b/charts/gohoarder/templates/pvc.yaml new file mode 100644 index 0000000..f16c7c4 --- /dev/null +++ b/charts/gohoarder/templates/pvc.yaml @@ -0,0 +1,37 @@ +{{- if and (eq .Values.storage.backend "filesystem") (not .Values.storage.filesystem.useHostPath) (not .Values.storage.filesystem.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "gohoarder.fullname" . }}-storage + labels: + {{- include "gohoarder.labels" . | nindent 4 }} + app.kubernetes.io/component: storage +spec: + accessModes: + - {{ .Values.storage.filesystem.accessMode }} + {{- if .Values.storage.filesystem.storageClass }} + storageClassName: {{ .Values.storage.filesystem.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.storage.filesystem.size | quote }} +{{- end }} +--- +{{- if and (eq .Values.metadata.backend "sqlite") .Values.metadata.sqlite.persistence.enabled (not .Values.metadata.sqlite.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "gohoarder.fullname" . }}-metadata + labels: + {{- include "gohoarder.labels" . | nindent 4 }} + app.kubernetes.io/component: metadata +spec: + accessModes: + - {{ .Values.metadata.sqlite.persistence.accessMode }} + {{- if .Values.metadata.sqlite.persistence.storageClass }} + storageClassName: {{ .Values.metadata.sqlite.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.metadata.sqlite.persistence.size | quote }} +{{- end }} diff --git a/charts/gohoarder/templates/secret.yaml b/charts/gohoarder/templates/secret.yaml new file mode 100644 index 0000000..cfa1876 --- /dev/null +++ b/charts/gohoarder/templates/secret.yaml @@ -0,0 +1,66 @@ +{{- if and .Values.auth.enabled (not .Values.auth.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gohoarder.fullname" . }}-auth + labels: + {{- include "gohoarder.labels" . | nindent 4 }} +type: Opaque +data: + {{- if .Values.auth.adminApiKey }} + {{ .Values.auth.secretKey }}: {{ .Values.auth.adminApiKey | b64enc | quote }} + {{- else }} + {{ .Values.auth.secretKey }}: {{ include "gohoarder.adminApiKey" . | b64enc | quote }} + {{- end }} +{{- end }} +--- +{{- if and (eq .Values.storage.backend "s3") (not .Values.storage.s3.existingSecret) .Values.storage.s3.accessKeyId }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gohoarder.fullname" . }}-s3 + labels: + {{- include "gohoarder.labels" . | nindent 4 }} +type: Opaque +data: + access-key-id: {{ .Values.storage.s3.accessKeyId | b64enc | quote }} + secret-access-key: {{ .Values.storage.s3.secretAccessKey | b64enc | quote }} +{{- end }} +--- +{{- if and (eq .Values.storage.backend "smb") (not .Values.storage.smb.existingSecret) .Values.storage.smb.username }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gohoarder.fullname" . }}-smb + labels: + {{- include "gohoarder.labels" . | nindent 4 }} +type: Opaque +data: + username: {{ .Values.storage.smb.username | b64enc | quote }} + password: {{ .Values.storage.smb.password | b64enc | quote }} +{{- end }} +--- +{{- if and (eq .Values.metadata.backend "postgresql") (not .Values.metadata.postgresql.existingSecret) .Values.metadata.postgresql.username }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gohoarder.fullname" . }}-postgresql + labels: + {{- include "gohoarder.labels" . | nindent 4 }} +type: Opaque +data: + username: {{ .Values.metadata.postgresql.username | b64enc | quote }} + password: {{ .Values.metadata.postgresql.password | b64enc | quote }} +{{- end }} +--- +{{- if and .Values.security.scanners.ghsa.enabled (not .Values.security.scanners.ghsa.existingSecret) .Values.security.scanners.ghsa.token }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gohoarder.fullname" . }}-ghsa + labels: + {{- include "gohoarder.labels" . | nindent 4 }} +type: Opaque +data: + token: {{ .Values.security.scanners.ghsa.token | b64enc | quote }} +{{- end }} diff --git a/charts/gohoarder/templates/service.yaml b/charts/gohoarder/templates/service.yaml new file mode 100644 index 0000000..7cb3848 --- /dev/null +++ b/charts/gohoarder/templates/service.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gohoarder.fullname" . }}-server + labels: + {{- include "gohoarder.server.labels" . | nindent 4 }} + {{- with .Values.server.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.server.service.type }} + ports: + - port: {{ .Values.server.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "gohoarder.server.selectorLabels" . | nindent 4 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gohoarder.fullname" . }}-frontend + labels: + {{- include "gohoarder.frontend.labels" . | nindent 4 }} + {{- with .Values.frontend.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.frontend.service.type }} + ports: + - port: {{ .Values.frontend.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "gohoarder.frontend.selectorLabels" . | nindent 4 }} diff --git a/charts/gohoarder/templates/serviceaccount.yaml b/charts/gohoarder/templates/serviceaccount.yaml new file mode 100644 index 0000000..facf516 --- /dev/null +++ b/charts/gohoarder/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "gohoarder.serviceAccountName" . }} + labels: + {{- include "gohoarder.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/gohoarder/values.yaml b/charts/gohoarder/values.yaml new file mode 100644 index 0000000..5e8a26d --- /dev/null +++ b/charts/gohoarder/values.yaml @@ -0,0 +1,475 @@ +# Default values for gohoarder +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Override the name of the chart +nameOverride: "" +# Override the full name of the chart +fullnameOverride: "" + +# Global configuration +global: + # Base domain for the deployment + domain: "gohoarder.local" + + # Image pull secrets for private registries + # Reference existing secrets by name: + # imagePullSecrets: + # - name: ghcr-secret + # - name: dockerhub-secret + imagePullSecrets: [] + +# Auto-create image pull secrets from credentials (optional) +# If you want the chart to create the secrets for you, use this instead: +# imageCredentials: +# ghcr-secret: +# registry: ghcr.io +# username: myusername +# password: mytoken +# email: myemail@example.com +# dockerhub-secret: +# registry: https://index.docker.io/v1/ +# username: myusername +# password: mytoken +# email: myemail@example.com +# Then reference them in global.imagePullSecrets: +# - name: ghcr-secret +imageCredentials: {} + +# Deployment replicas +# NOTE: When running multiple server replicas (>1): +# - Use S3 or SMB for storage.backend (not filesystem with local storage) +# - Use PostgreSQL for metadata.backend (SQLite has limited concurrency) +# - See "High Availability & Scaling" section in README +replicaCount: + server: 1 + frontend: 1 + scanner: 1 + +# Image configuration +image: + server: + repository: ghcr.io/lukaszraczylo/gohoarder-server + pullPolicy: IfNotPresent + tag: "0.0.1" + + frontend: + repository: ghcr.io/lukaszraczylo/gohoarder-frontend + pullPolicy: IfNotPresent + tag: "0.0.1" + + scanner: + repository: ghcr.io/lukaszraczylo/gohoarder-scanner + pullPolicy: IfNotPresent + tag: "0.0.1" + +# Service Account +serviceAccount: + create: true + annotations: {} + name: "" + +# Pod annotations +podAnnotations: {} + +# Pod security context +podSecurityContext: + fsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + +# Container security context +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + +# Server configuration +server: + host: "0.0.0.0" + port: 8080 + readTimeout: "5m" + writeTimeout: "5m" + idleTimeout: "2m" + + # Additional environment variables for server container + # Use this to override config via environment variables + # Format: GOHOARDER_ (dots replaced with underscores) + # Examples: + # GOHOARDER_STORAGE_BACKEND: s3 + # GOHOARDER_METADATA_BACKEND: postgresql + # env: + # - name: GOHOARDER_STORAGE_BACKEND + # value: "s3" + # - name: GOHOARDER_STORAGE_S3_BUCKET + # value: "my-bucket" + # - name: GOHOARDER_METADATA_POSTGRESQL_PASSWORD + # valueFrom: + # secretKeyRef: + # name: postgres-secret + # key: password + env: [] + + # Service configuration + service: + type: ClusterIP + port: 80 + targetPort: 8080 + annotations: {} + + # Resource limits + resources: + limits: + cpu: 2000m + memory: 2Gi + requests: + cpu: 500m + memory: 512Mi + + # Liveness and readiness probes + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health/ready + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + + # Node selector + nodeSelector: {} + + # Tolerations + tolerations: [] + + # Affinity + affinity: {} + +# Frontend configuration +frontend: + # Backend URL for API calls + backendUrl: "" # Auto-configured if empty + + # Additional environment variables for frontend container + # env: + # - name: API_BASE_URL + # value: "https://api.example.com" + env: [] + + # Service configuration + service: + type: ClusterIP + port: 80 + targetPort: 80 + annotations: {} + + # Resource limits + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + + # Liveness and readiness probes + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + + nodeSelector: {} + tolerations: [] + affinity: {} + +# Scanner configuration +scanner: + # Additional environment variables for scanner container + # env: + # - name: GOHOARDER_SECURITY_SCANNERS_TRIVY_ENABLED + # value: "true" + env: [] + + # Resource limits + resources: + limits: + cpu: 2000m + memory: 4Gi + requests: + cpu: 500m + memory: 1Gi + + nodeSelector: {} + tolerations: [] + affinity: {} + +# Storage configuration +storage: + # Storage backend: filesystem, s3, smb + # For multiple server replicas: + # - S3 or SMB (recommended) + # - Filesystem with ReadWriteMany (RWX) storage class (Longhorn, NFS, CephFS) + # - NOT filesystem with ReadWriteOnce (RWO) or local storage + backend: "filesystem" + + # Filesystem storage + filesystem: + # Storage class for PVC + # For multiple replicas: use RWX-capable storage class (longhorn, nfs-client, cephfs, etc.) + storageClass: "" + # Storage size + size: "100Gi" + # Access mode: + # ReadWriteOnce (RWO) - Single replica only + # ReadWriteMany (RWX) - Multiple replicas (requires RWX storage class) + accessMode: "ReadWriteOnce" + # Use hostPath instead of PVC (for single-node testing only) + useHostPath: false + hostPath: "/var/lib/gohoarder" + # Existing PVC name (if you want to use existing PVC) + existingClaim: "" + + # S3 storage + s3: + endpoint: "s3.amazonaws.com" + region: "us-east-1" + bucket: "gohoarder-cache" + accessKeyId: "" + secretAccessKey: "" + # Use existing secret for S3 credentials + existingSecret: "" + useSSL: true + + # SMB storage + smb: + host: "" + share: "" + username: "" + password: "" + domain: "" + # Use existing secret for SMB credentials + existingSecret: "" + +# Metadata storage configuration +metadata: + # Backend: sqlite, postgresql + # For multiple server replicas: postgresql is recommended (sqlite has concurrency limitations) + backend: "sqlite" + + # SQLite configuration + sqlite: + # Use PVC for SQLite database + persistence: + enabled: true + storageClass: "" + size: "10Gi" + accessMode: "ReadWriteOnce" + existingClaim: "" + walMode: true + + # PostgreSQL configuration + postgresql: + # Use bundled PostgreSQL (sets up postgresql subchart) + enabled: false + host: "localhost" + port: 5432 + database: "gohoarder" + username: "gohoarder" + password: "" + sslMode: "disable" + # Use existing secret for PostgreSQL credentials + existingSecret: "" + +# Cache configuration +cache: + defaultTTL: "168h" # 7 days + cleanupInterval: "1h" + maxSizeBytes: 536870912000 # 500GB + perProjectQuota: 53687091200 # 50GB + ttlOverrides: + npm: "168h" + pip: "168h" + go: "168h" + +# Security scanning configuration +security: + enabled: false + blockOnSeverity: "high" # none, low, medium, high, critical + scanOnDownload: true + rescanInterval: "24h" + updateDbOnStartup: false + + blockThresholds: + critical: 0 + high: -1 + medium: -1 + low: -1 + + scanners: + trivy: + enabled: false + timeout: "5m" + cacheDb: "/var/lib/trivy" + + osv: + enabled: false + apiUrl: "https://api.osv.dev" + timeout: "30s" + + grype: + enabled: false + timeout: "5m" + + govulncheck: + enabled: false + timeout: "5m" + + npmAudit: + enabled: false + timeout: "2m" + + pipAudit: + enabled: false + timeout: "2m" + + ghsa: + enabled: false + timeout: "30s" + # GitHub token for higher rate limits + token: "" + existingSecret: "" + + static: + enabled: true + maxPackageSize: 2147483648 # 2GB + checkChecksums: true + blockSuspicious: false + +# Authentication configuration +auth: + enabled: true + keyExpiration: "0" # Never expire + bcryptCost: 10 + auditLog: true + + # Admin API key - will be auto-generated if not provided + adminApiKey: "" + # Use existing secret for admin API key + existingSecret: "" + # Secret key name for admin API key + secretKey: "admin-api-key" + +# Network configuration +network: + connectTimeout: "10s" + readTimeout: "5m" + writeTimeout: "5m" + maxIdleConns: 100 + maxConnsPerHost: 10 + + rateLimit: + perApiKey: 1000 + perIp: 100 + burstSize: 50 + + circuitBreaker: + threshold: 5 + timeout: "30s" + resetInterval: "60s" + + retry: + maxAttempts: 3 + initialBackoff: "1s" + maxBackoff: "30s" + +# Logging configuration +logging: + level: "info" # debug, info, warn, error + format: "json" # json, pretty + +# Package handlers configuration +handlers: + go: + enabled: true + upstreamProxy: "https://proxy.golang.org" + checksumDb: "https://sum.golang.org" + verifyChecksums: true + + npm: + enabled: true + upstreamRegistry: "https://registry.npmjs.org" + + pypi: + enabled: true + upstreamUrl: "https://pypi.org" + simpleApiUrl: "https://pypi.org/simple" + +# Ingress configuration +ingress: + enabled: false + className: "nginx" + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/proxy-body-size: "2048m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "300" + nginx.ingress.kubernetes.io/proxy-send-timeout: "300" + + # Ingress for frontend + frontend: + enabled: true + host: "gohoarder.local" + tls: + enabled: false + secretName: "gohoarder-frontend-tls" + + # Ingress for API (if you want separate ingress) + api: + enabled: false + host: "api.gohoarder.local" + tls: + enabled: false + secretName: "gohoarder-api-tls" + +# Autoscaling configuration +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +# Pod Disruption Budget +podDisruptionBudget: + enabled: false + minAvailable: 1 + +# Network Policy +networkPolicy: + enabled: false + # Allow external access to server + ingress: + - from: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 8080 diff --git a/charts/packages/gohoarder-0.0.1.tgz b/charts/packages/gohoarder-0.0.1.tgz new file mode 100644 index 0000000..3666261 Binary files /dev/null and b/charts/packages/gohoarder-0.0.1.tgz differ diff --git a/index.yaml b/index.yaml index 8e1d61b..1b4f79b 100644 --- a/index.yaml +++ b/index.yaml @@ -1,5 +1,33 @@ apiVersion: v1 entries: + gohoarder: + - apiVersion: v2 + appVersion: 0.0.1 + created: "2026-01-02T23:23:54.075202022Z" + description: A universal package cache proxy supporting npm, PyPI, and Go modules + with security scanning + digest: 2271641c9d1e9942a02d94fe62b1c49d0835541160bf8abe198a1293ff4b811d + home: https://github.com/lukaszraczylo/gohoarder + icon: https://raw.githubusercontent.com/lukaszraczylo/gohoarder/main/docs/logo.png + keywords: + - package-manager + - cache + - proxy + - npm + - pypi + - go-modules + - security + - vulnerability-scanning + maintainers: + - email: lukasz@raczylo.com + name: Lukasz Raczylo + name: gohoarder + sources: + - https://github.com/lukaszraczylo/gohoarder + type: application + urls: + - https://github.com/lukaszraczylo/helm-charts/releases/download/gohoarder-0.0.1/gohoarder-0.0.1.tgz + version: 0.0.1 jobs-manager: - annotations: artifacthub.io/changes: | @@ -1623,4 +1651,4 @@ entries: urls: - https://github.com/lukaszraczylo/helm-charts/releases/download/kubemirror-0.2.8/kubemirror-0.2.8.tgz version: 0.2.8 -generated: "2025-12-27T03:40:40.629057295Z" +generated: "2026-01-02T23:23:54.07379072Z"