Public Access
Initial implementation: aptly container image, Compose stacks, Helm chart, and Gitea Actions pipelines
Provides a self-contained, containerized aptly (Debian repo manager) stack with independently releasable image and Helm chart versions. - images/: aptly-server (aptly built from source, cross-compiled) and aptly-deb-builder (nfpm + dpkg-buildpackage) container images - rootfs/: shared aptly-init/aptly-reconcile/aptly-push/aptly-pack scripts consumed identically by Compose and the Helm chart, driven by one declarative state.yaml contract - compose/: test (ephemeral, open) and production docker-compose stacks with an nginx read/auth sidecar - charts/aptly/: aptly-native Helm chart covering every security posture from fully open to authenticated read+write, Ingress and Gateway API support (usable in parallel for migration scenarios), metrics, and declarative repo/mirror/publish reconciliation via a Helm hook - .gitea/workflows/: CI (lint, template, kubeconform, E2E smoke test) plus separately tagged image (image/v*) and chart (chart/v*) releases, weekly rebuilds, and a preflight workflow validating the runner's Docker/Helm-OCI capabilities - pubkeys/: RSA chart-signing key for Helm --sign / Artifact Hub's signKey annotation (Helm can't verify Ed25519 keys) - docs/, README.md, charts/aptly/README.md: usage, security, and versioning documentation
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
apiVersion: v2
|
||||
name: aptly
|
||||
description: >-
|
||||
aptly (Debian repository management tool) on Kubernetes — a StatefulSet with
|
||||
an nginx read/auth sidecar, a fully aptly-native values API, and declarative
|
||||
repo/mirror/publish state reconciled via a Helm hook.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.6.3-1"
|
||||
home: https://git.morlana.online/f.weber/aptly-containerized
|
||||
sources:
|
||||
- https://git.morlana.online/f.weber/aptly-containerized
|
||||
- https://github.com/aptly-dev/aptly
|
||||
keywords:
|
||||
- aptly
|
||||
- apt
|
||||
- debian
|
||||
- package-repository
|
||||
- artifact-repository
|
||||
maintainers:
|
||||
- name: Florian Weber
|
||||
email: f.weber@flweber.me
|
||||
icon: https://www.aptly.info/img/aptly_medium.png
|
||||
annotations:
|
||||
artifacthub.io/license: MIT
|
||||
artifacthub.io/links: |
|
||||
- name: aptly upstream
|
||||
url: https://www.aptly.info/
|
||||
- name: Source
|
||||
url: https://git.morlana.online/f.weber/aptly-containerized
|
||||
artifacthub.io/signKey: |
|
||||
fingerprint: FC35C0FAA26605C4C21C7BBFBF43884145E5AA94
|
||||
url: https://git.morlana.online/f.weber/aptly-containerized/raw/branch/main/pubkeys/chart-signing.asc
|
||||
@@ -0,0 +1,395 @@
|
||||
# aptly
|
||||
|
||||
[aptly](https://www.aptly.info/) (Debian repository management tool) on Kubernetes:
|
||||
a single-pod StatefulSet (aptly + an nginx read/auth sidecar), a fully aptly-native
|
||||
values API, and declarative repo/mirror/publish state reconciled by a Helm hook —
|
||||
no library-chart dependency, no concepts to learn beyond aptly's and Kubernetes' own.
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
helm install my-aptly oci://git.morlana.online/f.weber/aptly --version <version>
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
This chart deploys [aptly](https://www.aptly.info/) as:
|
||||
|
||||
- a `StatefulSet` running two containers in one pod — `aptly` itself (bound to
|
||||
`127.0.0.1`, never reachable off-pod) and an `nginx` sidecar that is the only
|
||||
thing actually exposed, handling reads, auth, and the write-API proxy;
|
||||
- a `Service` in front of `nginx`, and optional `Ingress` and/or Gateway API
|
||||
`HTTPRoute` resources for apt clients and the API — both can be enabled at the
|
||||
same time, e.g. mid-migration between the two (see [Gateway API](#gateway-api));
|
||||
- a post-install/post-upgrade `Job` that reconciles the local repos, mirrors, and
|
||||
publish targets declared in `values.yaml` against the running instance's REST API.
|
||||
|
||||
Three things this chart is built around:
|
||||
|
||||
1. **A single security switch that actually reaches every mode**, including
|
||||
completely open (no auth on read *or* write) if that's what you want — see
|
||||
[Security modes](#security-modes) below.
|
||||
2. **An aptly-native config surface**: curated `aptly.*` keys for the common cases,
|
||||
plus `aptly.configOverrides` as a raw passthrough so any current or future aptly
|
||||
config key is reachable without waiting on a chart update.
|
||||
3. **Declarative state**: local repos, mirrors, and publish targets live in
|
||||
`values.yaml` and are converged towards on every `helm install`/`helm upgrade`,
|
||||
the same way the rest of the cluster is managed.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.24+
|
||||
- Helm 3.8+ (for OCI registry support) — Helm 4 also works
|
||||
- A `StorageClass` supporting `ReadWriteOnce`, unless `persistence.enabled: false`
|
||||
|
||||
## Installing the chart
|
||||
|
||||
```bash
|
||||
helm install my-aptly oci://git.morlana.online/f.weber/aptly --version <version> \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.repo.host=apt.example.com
|
||||
```
|
||||
|
||||
Without `ingress.enabled`, the post-install NOTES print a `kubectl port-forward`
|
||||
command instead — nothing has to be configured up front to try the chart out (e.g.
|
||||
in kind/k3d).
|
||||
|
||||
## Uninstalling the chart
|
||||
|
||||
```bash
|
||||
helm uninstall my-aptly
|
||||
```
|
||||
|
||||
The `PersistentVolumeClaim` created by this chart's `volumeClaimTemplate` (i.e.
|
||||
when `persistence.existingClaim` is unset) is **not** deleted — StatefulSet-owned
|
||||
PVCs never are, by Kubernetes' own design, regardless of any Helm annotation.
|
||||
Remove it yourself if you're done with the data — for a release named `my-aptly`,
|
||||
that PVC is `data-my-aptly-0` (`data-<statefulset-name>-<ordinal>`; find the exact
|
||||
name with `kubectl get pvc -l app.kubernetes.io/instance=<release>`):
|
||||
`kubectl delete pvc data-my-aptly-0`.
|
||||
|
||||
## Security modes
|
||||
|
||||
aptly itself ships with **no authentication at all**; this chart's `security.preset`
|
||||
switch controls what the nginx sidecar in front of it requires:
|
||||
|
||||
| `security.preset` | Read (apt clients) | Read auth | Write (`/api/`) | Write auth |
|
||||
|---|---|---|---|---|
|
||||
| `open` | on | **no** | on | **no** |
|
||||
| `publicRead` (**default**) | on | no | on | yes |
|
||||
| `authenticated` | on | yes | on | yes |
|
||||
| `readOnly` | on | no | **off (404)** | — |
|
||||
|
||||
```yaml
|
||||
security:
|
||||
preset: open # the fully-unsecured mode — one line, no confirmation gate
|
||||
```
|
||||
|
||||
`security.read.*` and `security.write.*` override the preset explicitly, so every
|
||||
individual cell of the matrix is reachable (e.g. `authenticated` but with anonymous
|
||||
reads, or `open` restricted to a CIDR on the write path). Health probes
|
||||
(`/api/ready`, `/api/healthy`) never require credentials, in any mode.
|
||||
|
||||
Credentials come from `security.auth.users` (plaintext, hashed into an
|
||||
`htpasswd` file by the initContainer at pod start — never store a pre-hashed
|
||||
password here, see the comment in `values.yaml`) or from
|
||||
`security.auth.existingSecret` (a pre-built `htpasswd` Secret key — the recommended
|
||||
production path, e.g. via ExternalSecrets/SealedSecrets).
|
||||
|
||||
Full write-up of the Ingress `split` mode, the `trustedProxies`/`allowCIDRs`
|
||||
pitfall behind an Ingress controller, and `write.inClusterOnly`:
|
||||
[docs/security.md](https://git.morlana.online/f.weber/aptly-containerized/src/branch/main/docs/security.md)
|
||||
in the repository.
|
||||
|
||||
## Gateway API
|
||||
|
||||
`gateway.*` is a complete, independent alternative to `ingress.*` — enable either
|
||||
one, or **both at once**. Nothing about this chart forces a choice, on purpose:
|
||||
if you're partway through migrating a cluster from Ingress to Gateway API, both
|
||||
resource sets can point at the same `Service` for as long as that takes.
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
enabled: true
|
||||
parentRefs:
|
||||
- name: my-gateway # a Gateway your cluster admin already manages —
|
||||
namespace: gateway-infra # this chart never creates one itself
|
||||
repo:
|
||||
hostnames: [apt.example.com]
|
||||
```
|
||||
|
||||
This mirrors `ingress.*` concept for concept:
|
||||
|
||||
- `gateway.mode: single` (default) creates one `HTTPRoute` that carries both apt
|
||||
reads and `/api/` writes — nginx does the split internally, exactly as in
|
||||
Ingress `single` mode.
|
||||
- `gateway.mode: split` creates a second `HTTPRoute` for `/api/` under
|
||||
`gateway.api.hostnames`, optionally attached to a **different** Gateway via
|
||||
`gateway.api.parentRefs` (falls back to `gateway.parentRefs` when unset) — e.g.
|
||||
an internal-only Gateway for the write path. Same caveat as Ingress `split`
|
||||
mode applies: this is a route-level split, not something nginx itself enforces,
|
||||
so combine it with `security.write.inClusterOnly` or a `NetworkPolicy` if a
|
||||
request arriving on the wrong hostname must actually be rejected at the network
|
||||
level.
|
||||
- `security.write.inClusterOnly: true` omits the API `HTTPRoute` entirely in
|
||||
`split` mode, the same way it omits the API `Ingress`.
|
||||
- The shared `proxy.enabled=false` guard applies here too: a `HTTPRoute` (or
|
||||
`Ingress`) in front of aptly's unauthenticated write API is refused unless
|
||||
`security.preset: open` confirms it's intended.
|
||||
|
||||
One real difference from Ingress: **TLS is not configured here.** Gateway API
|
||||
deliberately separates infrastructure (the `Gateway` and its listeners, owned by
|
||||
a cluster admin) from routing (the `HTTPRoute`, owned by this chart) — so TLS
|
||||
termination is the referenced `Gateway`'s job, not a `gateway.repo.tls`-style
|
||||
field this chart would need to expose.
|
||||
|
||||
Core Gateway API resources have been GA (`gateway.networking.k8s.io/v1`) since
|
||||
v1.0; `gateway.apiVersion` exists as an escape hatch only if your cluster's CRDs
|
||||
still predate that.
|
||||
|
||||
## Configuring aptly itself
|
||||
|
||||
Two layers, always merged in this order — nothing in aptly's own configuration is
|
||||
ever unreachable through this chart:
|
||||
|
||||
1. curated `aptly.*` keys (omitted from the rendered config when unset, so a
|
||||
default install matches aptly's own upstream defaults exactly);
|
||||
2. `aptly.configOverrides` — raw aptly YAML (snake_case keys, same as aptly's own
|
||||
config file), deep-merged over the generated config last, always wins.
|
||||
|
||||
```yaml
|
||||
aptly:
|
||||
architectures: [amd64, arm64]
|
||||
metrics:
|
||||
enabled: true
|
||||
configOverrides:
|
||||
s3_publish_endpoints:
|
||||
cdn:
|
||||
region: eu-central-1
|
||||
bucket: apt-example
|
||||
```
|
||||
|
||||
## Declarative state
|
||||
|
||||
Local repos, mirrors, and publish targets declared under `aptly.*` are converged
|
||||
towards by a Helm hook Job on every install/upgrade — talking only to aptly's REST
|
||||
API, never the CLI (the API server holds aptly's database lock).
|
||||
|
||||
```yaml
|
||||
aptly:
|
||||
localRepos:
|
||||
- name: stable
|
||||
defaultDistribution: stable
|
||||
defaultComponent: main
|
||||
publish:
|
||||
- name: stable-root
|
||||
prefix: ""
|
||||
distribution: stable
|
||||
sourceKind: local
|
||||
sources: [{ name: stable, component: main }]
|
||||
architectures: [amd64, arm64]
|
||||
```
|
||||
|
||||
Known limitation: `mirrors[].components` cannot be changed after a mirror is
|
||||
created (aptly's API has no endpoint for that) — changing it means deleting and
|
||||
recreating the mirror. Everything else is kept in sync on every run.
|
||||
|
||||
## GPG signing
|
||||
|
||||
```yaml
|
||||
aptly:
|
||||
gpg:
|
||||
enabled: true # false = gpg_disable_sign + Signing.Skip on every publish call
|
||||
provider: gpg # gpg (default) | internal (pure-Go, no gnupg binary)
|
||||
signingKey:
|
||||
existingSecret: my-signing-key # keys: privateKey (or secretKeyring), passphrase
|
||||
```
|
||||
|
||||
The public key is served at `aptly.gpg.publishPublicKey.path` (default
|
||||
`/signing-key.asc`) so clients can fetch it directly:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://apt.example.com/signing-key.asc | gpg --dearmor \
|
||||
| sudo tee /usr/share/keyrings/example.gpg >/dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/example.gpg] https://apt.example.com/ stable main' \
|
||||
| sudo tee /etc/apt/sources.list.d/example.list
|
||||
```
|
||||
|
||||
Never put a real private key inline in `values.yaml` — `aptly.gpg.signingKey.privateKey`
|
||||
exists only as a quick-test escape hatch.
|
||||
|
||||
## Storage
|
||||
|
||||
`persistence.size`/`persistence.storageClass` are **immutable** once installed —
|
||||
Kubernetes forbids changing a StatefulSet's `volumeClaimTemplates` in place. Set
|
||||
`persistence.existingClaim` from the start in production: then no
|
||||
`volumeClaimTemplate` exists at all, and resizing the referenced PVC directly is a
|
||||
plain, supported operation.
|
||||
|
||||
## Parameters
|
||||
|
||||
### Image
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `image.repository` | `git.morlana.online/f.weber/aptly` | aptly-server image |
|
||||
| `image.tag` | `""` | falls back to `.Chart.AppVersion` — the last released image, never `latest` |
|
||||
| `image.pullPolicy` | `IfNotPresent` | |
|
||||
| `image.pullSecrets` | `[]` | |
|
||||
| `nginx.image.repository` | `nginxinc/nginx-unprivileged` | upstream image, unmodified |
|
||||
| `nginx.image.tag` | `1-alpine` | |
|
||||
| `nginx.image.pullPolicy` | `IfNotPresent` | |
|
||||
| `nginx.resources` | `{}` | |
|
||||
| `nginx.securityContext` | non-root, all caps dropped | |
|
||||
|
||||
### aptly configuration
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `aptly.architectures` | `[]` | empty = all available |
|
||||
| `aptly.logLevel` | `info` | |
|
||||
| `aptly.logFormat` | `json` | |
|
||||
| `aptly.download.concurrency` | `4` | |
|
||||
| `aptly.download.limit` | `0` | KB/s, `0` = unlimited |
|
||||
| `aptly.download.retries` | `0` | |
|
||||
| `aptly.download.sourcePackages` | `false` | |
|
||||
| `aptly.publishing.skipContents` | `false` | |
|
||||
| `aptly.publishing.skipBz2` | `false` | |
|
||||
| `aptly.metrics.enabled` | `false` | exposes `/api/metrics` on a separate, unauthenticated port — see [Metrics](#metrics) |
|
||||
| `aptly.swagger.enabled` | `false` | exposes `/docs.html` |
|
||||
| `aptly.gpg.enabled` | `true` | `false` disables signing entirely — see [GPG signing](#gpg-signing) |
|
||||
| `aptly.gpg.verify` | `true` | mirror signature verification |
|
||||
| `aptly.gpg.provider` | `gpg` | `gpg` \| `internal` |
|
||||
| `aptly.gpg.signingKey.existingSecret` | `""` | Secret key `privateKey` or `secretKeyring`, optional `passphrase` |
|
||||
| `aptly.gpg.signingKey.privateKey` | `""` | inline armored key — quick tests only, never for production |
|
||||
| `aptly.gpg.signingKey.passphrase` | `""` | only used with the inline key above |
|
||||
| `aptly.gpg.publishPublicKey.enabled` | `true` | serve the public key over HTTP |
|
||||
| `aptly.gpg.publishPublicKey.path` | `/signing-key.asc` | |
|
||||
| `aptly.gpgKeys` | `[]` | trusted keys imported for mirror verification — `[{name, armored}]` |
|
||||
| `aptly.localRepos` | `[]` | see [Declarative state](#declarative-state) |
|
||||
| `aptly.mirrors` | `[]` | see [Declarative state](#declarative-state) |
|
||||
| `aptly.publish` | `[]` | see [Declarative state](#declarative-state) |
|
||||
| `aptly.configOverrides` | `{}` | raw aptly config, deep-merged last — see [Configuring aptly itself](#configuring-aptly-itself) |
|
||||
| `aptly.existingSecretEnv` | `[]` | Secret names to `envFrom`, for `${VAR}` placeholders inside `configOverrides` |
|
||||
|
||||
### Security
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `security.preset` | `publicRead` | `open` \| `publicRead` \| `authenticated` \| `readOnly` — see [Security modes](#security-modes) |
|
||||
| `security.auth.users` | `{}` | `name: plaintext-password` map, hashed at pod start |
|
||||
| `security.auth.existingSecret` | `""` | pre-built `htpasswd` Secret — wins over `users` |
|
||||
| `security.auth.internalUser.enabled` | `true` | credentials the reconcile Job authenticates through nginx with, in every preset |
|
||||
| `security.auth.internalUser.username` | `aptly-internal` | |
|
||||
| `security.trustedProxies` | `[]` | CIDRs to trust `X-Forwarded-For` from — required for `allowCIDRs` to be meaningful behind an Ingress |
|
||||
| `security.read.enabled` | `true` | overrides the preset |
|
||||
| `security.read.requireAuth` | `null` | `null` = preset's value |
|
||||
| `security.read.allowCIDRs` | `[]` | |
|
||||
| `security.write.enabled` | `true` | overrides the preset |
|
||||
| `security.write.requireAuth` | `null` | `null` = preset's value |
|
||||
| `security.write.allowCIDRs` | `[]` | |
|
||||
| `security.write.inClusterOnly` | `false` | in `ingress.mode: split`, omits the API Ingress entirely |
|
||||
|
||||
### Proxy
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `proxy.enabled` | `true` | `false` = aptly serves reads itself (`serve_in_api_mode`), no nginx split — only sane with `security.preset: open` |
|
||||
| `proxy.compatPaths` | `true` | also serve the tree under `/repos/<name>/`, aptly's own URL shape |
|
||||
| `proxy.maxUploadSize` | `"0"` | nginx `client_max_body_size`, `"0"` = unlimited |
|
||||
| `proxy.readTimeout` | `3600s` | |
|
||||
| `proxy.publishEndpointName` | `public` | the `filesystem_publish_endpoints` key aptly publishes under |
|
||||
|
||||
### Persistence
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `persistence.enabled` | `true` | `false` = `emptyDir` |
|
||||
| `persistence.existingClaim` | `""` | **set this in production** — see [Storage](#storage) |
|
||||
| `persistence.storageClass` | `""` | falls back to `global.defaultStorageClass` |
|
||||
| `persistence.accessMode` | `ReadWriteOnce` | |
|
||||
| `persistence.size` | `20Gi` | immutable once installed unless using `existingClaim` |
|
||||
| `persistence.annotations` | `{}` | |
|
||||
|
||||
### Workload
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `workload.updateStrategy.type` | `RollingUpdate` | safe with `replicas: 1` on a StatefulSet |
|
||||
| `workload.podManagementPolicy` | `OrderedReady` | |
|
||||
| `workload.revisionHistoryLimit` | `3` | |
|
||||
| `workload.terminationGracePeriodSeconds` | `60` | |
|
||||
| `workload.annotations` / `podAnnotations` / `podLabels` | `{}` | |
|
||||
| `podSecurityContext` | non-root, uid/gid 10001 | |
|
||||
| `containerSecurityContext` | all caps dropped, read-only root fs | |
|
||||
| `resources` | `{}` | the `aptly` container |
|
||||
| `probes.startup` / `.readiness` / `.liveness` | see `values.yaml` | tuned for LevelDB recovery time on unclean shutdown |
|
||||
|
||||
### Networking
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `service.type` | `ClusterIP` | |
|
||||
| `service.port` | `8080` | |
|
||||
| `service.annotations` | `{}` | |
|
||||
| `ingress.enabled` | `false` | |
|
||||
| `ingress.mode` | `single` | `single` \| `split` — see [docs/security.md](https://git.morlana.online/f.weber/aptly-containerized/src/branch/main/docs/security.md) |
|
||||
| `ingress.className` | `""` | |
|
||||
| `ingress.annotations` | body-size/read-timeout defaults for nginx-ingress | |
|
||||
| `ingress.repo.host` / `.path` / `.pathType` / `.tls` | | apt-client-facing Ingress |
|
||||
| `ingress.api.enabled` / `.host` / `.className` / `.annotations` / `.tls` | | `split` mode only |
|
||||
| `gateway.enabled` | `false` | independent of `ingress.enabled` — both may be `true` at once, see [Gateway API](#gateway-api) |
|
||||
| `gateway.apiVersion` | `gateway.networking.k8s.io/v1` | override only for pre-GA clusters |
|
||||
| `gateway.mode` | `single` | `single` \| `split` — same meaning as `ingress.mode` |
|
||||
| `gateway.parentRefs` | `[]` | required when `gateway.enabled: true` — `[{name, namespace, sectionName}]` |
|
||||
| `gateway.repo.hostnames` / `.path` / `.pathType` | | apt-client-facing `HTTPRoute`; `pathType` is Gateway API's own enum (`PathPrefix`/`Exact`/`RegularExpression`), distinct from `ingress.repo.pathType`'s |
|
||||
| `gateway.api.enabled` / `.hostnames` / `.parentRefs` | | `split` mode only; `.parentRefs` falls back to `gateway.parentRefs` when unset |
|
||||
| `networkPolicy.enabled` | `false` | |
|
||||
| `networkPolicy.allowedNamespaces` | `[]` | empty = no ingress restriction |
|
||||
| `networkPolicy.extraIngress` | `[]` | |
|
||||
| `networkPolicy.egress.allowAll` | `true` | disabling this breaks mirrors unless you add `egress.extra` rules yourself |
|
||||
|
||||
### Metrics
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `metrics.service.enabled` | `false` | separate, unauthenticated port |
|
||||
| `metrics.service.port` | `9090` | |
|
||||
| `metrics.serviceMonitor.enabled` | `false` | requires the Prometheus Operator CRDs |
|
||||
| `metrics.serviceMonitor.interval` | `30s` | |
|
||||
|
||||
### Reconcile
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `reconcile.enabled` | `true` | |
|
||||
| `reconcile.mode` | `hook` | `hook` \| `job` (GitOps-friendly, hashed name) \| `manual` |
|
||||
| `reconcile.failOnError` | `false` | `true` makes an unreachable mirror fail the release |
|
||||
| `reconcile.timeoutSeconds` | `600` | |
|
||||
| `reconcile.image` | `{}` | overrides `repository`/`tag`/`pullPolicy`; defaults to the main `image` |
|
||||
| `reconcile.resources` | `{}` | |
|
||||
|
||||
### Pod disruption & scheduling
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `podDisruptionBudget.enabled` | `false` | |
|
||||
| `podDisruptionBudget.maxUnavailable` | `1` | never set `minAvailable` here — with `replicas: 1` it blocks every node drain forever |
|
||||
| `nodeSelector` / `tolerations` / `affinity` / `topologySpreadConstraints` / `priorityClassName` | | standard scheduling escape hatches |
|
||||
|
||||
### Escape hatches
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `extraEnv` / `extraEnvFrom` | `[]` | on the `aptly` container |
|
||||
| `extraVolumes` / `extraVolumeMounts` | `[]` | |
|
||||
| `extraInitContainers` / `extraContainers` | `[]` | |
|
||||
| `global.imageRegistry` | `""` | prefixes both `image.repository` and `nginx.image.repository` |
|
||||
| `global.imagePullSecrets` | `[]` | |
|
||||
| `global.defaultStorageClass` | `""` | |
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](https://git.morlana.online/f.weber/aptly-containerized/src/branch/main/LICENSE)
|
||||
and [NOTICE.md](https://git.morlana.online/f.weber/aptly-containerized/src/branch/main/NOTICE.md)
|
||||
in the repository.
|
||||
@@ -0,0 +1,5 @@
|
||||
security:
|
||||
preset: authenticated
|
||||
auth:
|
||||
users:
|
||||
ci: "changeme"
|
||||
@@ -0,0 +1,82 @@
|
||||
# Exercises: existingClaim, existingSecret (auth + gpg), metrics + ServiceMonitor,
|
||||
# networkPolicy, PDB, declarative state (gpgKeys/localRepos/mirrors/publish),
|
||||
# configOverrides passthrough, extra* escape hatches.
|
||||
persistence:
|
||||
existingClaim: my-existing-pvc
|
||||
|
||||
security:
|
||||
preset: authenticated
|
||||
auth:
|
||||
existingSecret: my-htpasswd-secret
|
||||
trustedProxies: ["10.0.0.0/8"]
|
||||
write:
|
||||
allowCIDRs: ["10.42.0.0/16"]
|
||||
|
||||
aptly:
|
||||
architectures: [amd64, arm64]
|
||||
metrics:
|
||||
enabled: true
|
||||
gpg:
|
||||
signingKey:
|
||||
existingSecret: my-signing-key-secret
|
||||
gpgKeys:
|
||||
- name: debian-archive
|
||||
armored: |
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
dGVzdA==
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
localRepos:
|
||||
- name: stable
|
||||
comment: "Production repo"
|
||||
defaultDistribution: stable
|
||||
defaultComponent: main
|
||||
mirrors:
|
||||
- name: debian-security
|
||||
archiveURL: http://security.debian.org/debian-security
|
||||
distribution: trixie-security
|
||||
components: [main]
|
||||
architectures: [amd64, arm64]
|
||||
publish:
|
||||
- name: stable-root
|
||||
prefix: ""
|
||||
distribution: stable
|
||||
sourceKind: local
|
||||
sources: [{ name: stable, component: main }]
|
||||
architectures: [amd64, arm64]
|
||||
acquireByHash: true
|
||||
configOverrides:
|
||||
download_concurrency: 8
|
||||
existingSecretEnv: ["some-other-secret"]
|
||||
|
||||
metrics:
|
||||
service:
|
||||
enabled: true
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
allowedNamespaces: ["ci", "monitoring"]
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
maxUnavailable: 1
|
||||
|
||||
reconcile:
|
||||
mode: job
|
||||
|
||||
extraEnv:
|
||||
- name: FOO
|
||||
value: bar
|
||||
extraVolumes:
|
||||
- name: extra
|
||||
emptyDir: {}
|
||||
extraVolumeMounts:
|
||||
- name: extra
|
||||
mountPath: /extra
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
tolerations:
|
||||
- key: "example"
|
||||
operator: "Exists"
|
||||
priorityClassName: "high-priority"
|
||||
@@ -0,0 +1,7 @@
|
||||
gateway:
|
||||
enabled: true
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
namespace: gateway-infra
|
||||
repo:
|
||||
hostnames: [apt.example.com]
|
||||
@@ -0,0 +1,20 @@
|
||||
gateway:
|
||||
enabled: true
|
||||
mode: split
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
namespace: gateway-infra
|
||||
repo:
|
||||
hostnames: [apt.example.com]
|
||||
api:
|
||||
enabled: true
|
||||
hostnames: [aptly-api.example.com]
|
||||
parentRefs:
|
||||
- name: internal-gateway
|
||||
namespace: gateway-infra
|
||||
|
||||
security:
|
||||
preset: authenticated
|
||||
auth:
|
||||
users:
|
||||
ci: "changeme"
|
||||
@@ -0,0 +1,14 @@
|
||||
# Mid-migration scenario: Ingress and Gateway API enabled at the same time,
|
||||
# both pointing at the same Service. Proves the two never conflict.
|
||||
ingress:
|
||||
enabled: true
|
||||
repo:
|
||||
host: apt.example.com
|
||||
|
||||
gateway:
|
||||
enabled: true
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
namespace: gateway-infra
|
||||
repo:
|
||||
hostnames: [apt.example.com]
|
||||
@@ -0,0 +1,5 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
mode: single
|
||||
repo:
|
||||
host: apt.example.com
|
||||
@@ -0,0 +1,13 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
mode: split
|
||||
repo:
|
||||
host: apt.example.com
|
||||
api:
|
||||
enabled: true
|
||||
host: aptly-api.example.com
|
||||
security:
|
||||
preset: authenticated
|
||||
auth:
|
||||
users:
|
||||
ci: "changeme"
|
||||
@@ -0,0 +1,7 @@
|
||||
security:
|
||||
preset: open
|
||||
proxy:
|
||||
enabled: false
|
||||
aptly:
|
||||
gpg:
|
||||
enabled: false
|
||||
@@ -0,0 +1,5 @@
|
||||
security:
|
||||
preset: open
|
||||
aptly:
|
||||
gpg:
|
||||
enabled: false
|
||||
@@ -0,0 +1,2 @@
|
||||
security:
|
||||
preset: readOnly
|
||||
@@ -0,0 +1,89 @@
|
||||
{{- $fullname := include "aptly.fullname" . -}}
|
||||
{{- $sec := include "aptly.security" . | fromJson -}}
|
||||
aptly ({{ .Chart.AppVersion }}, chart {{ .Chart.Version }}) is deploying as {{ $fullname }}-0 in {{ .Release.Namespace }}.
|
||||
|
||||
{{- if eq .Values.security.preset "open" }}
|
||||
|
||||
*** security.preset: open ***
|
||||
Both reading AND writing (the mutating /api/ path) are reachable with NO
|
||||
authentication from anything that can reach the Service — this is exactly
|
||||
the "komplett unabgesichert" mode, working as configured. Nothing further
|
||||
to set up; just make sure this is really what you want before exposing it
|
||||
beyond your own network.
|
||||
{{- end }}
|
||||
|
||||
--- Check it's up -------------------------------------------------------
|
||||
|
||||
kubectl exec -n {{ .Release.Namespace }} {{ $fullname }}-0 -c aptly -- \
|
||||
curl -fsS http://127.0.0.1:8080/api/ready
|
||||
|
||||
kubectl logs -n {{ .Release.Namespace }} job/{{ $fullname }}-reconcile
|
||||
# (only present right after install/upgrade in `hook` mode)
|
||||
|
||||
--- Reach it -------------------------------------------------------------
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- with .Values.ingress.repo.host }}
|
||||
|
||||
https://{{ . }}/ (Ingress)
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.enabled }}
|
||||
{{- range .Values.gateway.repo.hostnames }}
|
||||
|
||||
https://{{ . }}/ (Gateway API)
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not (or .Values.ingress.enabled .Values.gateway.enabled) }}
|
||||
|
||||
kubectl port-forward -n {{ .Release.Namespace }} svc/{{ $fullname }} 8080:{{ .Values.service.port }}
|
||||
# then use http://127.0.0.1:8080/ below
|
||||
{{- end }}
|
||||
|
||||
--- Configure apt on a client ---------------------------------------------
|
||||
|
||||
{{- $host := "apt.example.com" }}
|
||||
{{- if and .Values.ingress.enabled .Values.ingress.repo.host }}
|
||||
{{- $host = .Values.ingress.repo.host }}
|
||||
{{- else if and .Values.gateway.enabled .Values.gateway.repo.hostnames }}
|
||||
{{- $host = first .Values.gateway.repo.hostnames }}
|
||||
{{- end }}
|
||||
{{- if .Values.aptly.gpg.enabled }}
|
||||
{{- if .Values.aptly.gpg.publishPublicKey.enabled }}
|
||||
|
||||
curl -fsSL https://{{ $host }}{{ .Values.aptly.gpg.publishPublicKey.path }} \
|
||||
| gpg --dearmor | sudo tee /usr/share/keyrings/{{ include "aptly.name" . }}.gpg >/dev/null
|
||||
|
||||
echo 'deb [signed-by=/usr/share/keyrings/{{ include "aptly.name" . }}.gpg] https://{{ $host }}/ <dist> <component>' \
|
||||
| sudo tee /etc/apt/sources.list.d/{{ include "aptly.name" . }}.list
|
||||
{{- else }}
|
||||
|
||||
aptly.gpg.enabled=true but aptly.gpg.publishPublicKey.enabled=false — the
|
||||
signing key is not being served; distribute it to clients yourself.
|
||||
echo 'deb [signed-by=/path/to/your-key.gpg] https://{{ $host }}/ <dist> <component>' \
|
||||
| sudo tee /etc/apt/sources.list.d/{{ include "aptly.name" . }}.list
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
|
||||
echo 'deb [trusted=yes] https://{{ $host }}/ <dist> <component>' \
|
||||
| sudo tee /etc/apt/sources.list.d/{{ include "aptly.name" . }}.list
|
||||
{{- end }}
|
||||
{{- if $sec.ra }}
|
||||
|
||||
Reads require credentials in this preset ({{ .Values.security.preset }}):
|
||||
echo 'machine {{ $host }} login <user> password <password>' \
|
||||
| sudo tee -a /etc/apt/auth.conf.d/{{ include "aptly.name" . }}.conf
|
||||
{{- end }}
|
||||
|
||||
--- Resizing storage later -------------------------------------------------
|
||||
|
||||
persistence.size on an already-installed StatefulSet is IMMUTABLE via
|
||||
`helm upgrade` (Kubernetes forbids changing volumeClaimTemplates in place).
|
||||
For production, set persistence.existingClaim to a PVC you manage yourself
|
||||
— resizing that is a plain PVC edit. See docs/operations.md for the
|
||||
recovery procedure if you need to resize a chart-managed PVC anyway.
|
||||
{{- if and .Values.podDisruptionBudget.enabled (le (int .Values.podDisruptionBudget.maxUnavailable) 0) }}
|
||||
|
||||
*** podDisruptionBudget.maxUnavailable is 0 with replicas=1 — this blocks
|
||||
every voluntary node drain forever. ***
|
||||
{{- end }}
|
||||
@@ -0,0 +1,263 @@
|
||||
{{/*
|
||||
Standard name/label helpers, bookstack-chart style.
|
||||
*/}}
|
||||
{{- define "aptly.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "aptly.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 -}}
|
||||
|
||||
{{- define "aptly.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "aptly.labels" -}}
|
||||
helm.sh/chart: {{ include "aptly.chart" . }}
|
||||
{{ include "aptly.selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "aptly.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "aptly.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Image helpers — global.imageRegistry prefixes the repository when set,
|
||||
matching the bookstack-chart convention.
|
||||
*/}}
|
||||
{{- define "aptly.image" -}}
|
||||
{{- $registry := .Values.global.imageRegistry -}}
|
||||
{{- $repo := .Values.image.repository -}}
|
||||
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
|
||||
{{- if $registry -}}
|
||||
{{- printf "%s/%s:%s" $registry $repo $tag -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" $repo $tag -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "aptly.nginxImage" -}}
|
||||
{{- $registry := .Values.global.imageRegistry -}}
|
||||
{{- $repo := .Values.nginx.image.repository -}}
|
||||
{{- $tag := .Values.nginx.image.tag -}}
|
||||
{{- if $registry -}}
|
||||
{{- printf "%s/%s:%s" $registry $repo $tag -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" $repo $tag -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Shared by templates/ingress.yaml and templates/httproute.yaml: proxy.enabled=false
|
||||
means aptly's unauthenticated write API sits directly behind whatever routes to
|
||||
it, on a single listener with no path-based auth split possible — refuse to wire
|
||||
that up to an Ingress OR a Gateway API HTTPRoute unless security.preset=open
|
||||
confirms it's intended.
|
||||
*/}}
|
||||
{{- define "aptly.exposureGuard" -}}
|
||||
{{- if and (not .Values.proxy.enabled) (or .Values.ingress.enabled .Values.gateway.enabled) (ne .Values.security.preset "open") -}}
|
||||
{{- fail "proxy.enabled=false publishes aptly's unauthenticated write API through the Ingress/HTTPRoute (no path-based auth split is possible on a single listener). Set security.preset=open to confirm this is intended, or keep proxy.enabled=true." -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "aptly.imagePullSecrets" -}}
|
||||
{{- $secrets := concat (.Values.global.imagePullSecrets | default list) (.Values.image.pullSecrets | default list) -}}
|
||||
{{- if $secrets }}
|
||||
imagePullSecrets:
|
||||
{{- range $secrets }}
|
||||
- name: {{ if kindIs "map" . }}{{ .name }}{{ else }}{{ . }}{{ end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Whether aptly serves the published tree itself (no nginx proxy in front).
|
||||
Only sane in combination with security.preset=open — enforced in
|
||||
templates/statefulset.yaml (the aptly config) and templates/ingress.yaml
|
||||
(the hard `fail` guard).
|
||||
*/}}
|
||||
{{- define "aptly.serveInApiMode" -}}
|
||||
{{- if .Values.proxy.enabled -}}false{{- else -}}true{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "aptly.apiListen" -}}
|
||||
{{- if .Values.proxy.enabled -}}127.0.0.1:8080{{- else -}}0.0.0.0:8080{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Resolve security.preset + explicit read/write overrides into a plain dict
|
||||
{r, ra, w, wa} (read-enabled, read-auth, write-enabled, write-auth). Both
|
||||
nginx.conf.tpl and templates/ingress.yaml key off this so preset expansion
|
||||
lives in exactly one place.
|
||||
*/}}
|
||||
{{- define "aptly.security" -}}
|
||||
{{- $p := .Values.security.preset -}}
|
||||
{{- $presets := dict
|
||||
"open" (dict "r" true "ra" false "w" true "wa" false)
|
||||
"publicRead" (dict "r" true "ra" false "w" true "wa" true)
|
||||
"authenticated" (dict "r" true "ra" true "w" true "wa" true)
|
||||
"readOnly" (dict "r" true "ra" false "w" false "wa" false)
|
||||
-}}
|
||||
{{- $base := index $presets $p -}}
|
||||
{{- if not $base -}}
|
||||
{{- fail (printf "security.preset: unknown value %q (must be one of open, publicRead, authenticated, readOnly)" $p) -}}
|
||||
{{- end -}}
|
||||
{{- $d := deepCopy $base -}}
|
||||
{{- if kindIs "bool" .Values.security.read.enabled }}{{- $_ := set $d "r" .Values.security.read.enabled -}}{{- end -}}
|
||||
{{- if kindIs "bool" .Values.security.read.requireAuth }}{{- $_ := set $d "ra" .Values.security.read.requireAuth -}}{{- end -}}
|
||||
{{- if kindIs "bool" .Values.security.write.enabled }}{{- $_ := set $d "w" .Values.security.write.enabled -}}{{- end -}}
|
||||
{{- if kindIs "bool" .Values.security.write.requireAuth }}{{- $_ := set $d "wa" .Values.security.write.requireAuth -}}{{- end -}}
|
||||
{{- $d | toJson -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the aptly config (YAML) from the curated values.aptly.* keys, then
|
||||
deep-merge aptly.configOverrides on top so every current/future aptly config
|
||||
key stays reachable without a chart change. Curated keys are OMITTED when
|
||||
unset, so a default install matches aptly's own upstream defaults exactly
|
||||
(see utils/config.go in aptly-dev/aptly for the canonical defaults).
|
||||
*/}}
|
||||
{{- define "aptly.config" -}}
|
||||
{{- $v := .Values.aptly -}}
|
||||
{{- $c := dict "root_dir" "/var/lib/aptly" -}}
|
||||
{{- with $v.logLevel }}{{- $_ := set $c "log_level" . -}}{{- end -}}
|
||||
{{- with $v.logFormat }}{{- $_ := set $c "log_format" . -}}{{- end -}}
|
||||
{{- if $v.architectures }}{{- $_ := set $c "architectures" $v.architectures -}}{{- end -}}
|
||||
{{- with $v.download.concurrency }}{{- $_ := set $c "download_concurrency" . -}}{{- end -}}
|
||||
{{- with $v.download.limit }}{{- $_ := set $c "download_limit" . -}}{{- end -}}
|
||||
{{- with $v.download.retries }}{{- $_ := set $c "download_retries" . -}}{{- end -}}
|
||||
{{- if kindIs "bool" $v.download.sourcePackages }}{{- $_ := set $c "download_sourcepackages" $v.download.sourcePackages -}}{{- end -}}
|
||||
{{- $_ := set $c "gpg_provider" ($v.gpg.provider | default "gpg") -}}
|
||||
{{- $_ := set $c "gpg_disable_sign" (not $v.gpg.enabled) -}}
|
||||
{{- $_ := set $c "gpg_disable_verify" (not $v.gpg.verify) -}}
|
||||
{{- if kindIs "bool" $v.publishing.skipContents }}{{- $_ := set $c "skip_contents_publishing" $v.publishing.skipContents -}}{{- end -}}
|
||||
{{- if kindIs "bool" $v.publishing.skipBz2 }}{{- $_ := set $c "skip_bz2_publishing" $v.publishing.skipBz2 -}}{{- end -}}
|
||||
{{- $_ := set $c "enable_metrics_endpoint" ($v.metrics.enabled | default false) -}}
|
||||
{{- $_ := set $c "enable_swagger_endpoint" ($v.swagger.enabled | default false) -}}
|
||||
{{- $_ := set $c "serve_in_api_mode" (eq (include "aptly.serveInApiMode" .) "true") -}}
|
||||
{{- $_ := set $c "filesystem_publish_endpoints" (dict $.Values.proxy.publishEndpointName (dict "root_dir" "/var/lib/aptly/public" "link_method" "hardlink")) -}}
|
||||
{{- $merged := mergeOverwrite $c (deepCopy ($v.configOverrides | default dict)) -}}
|
||||
{{- toYaml $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render nginx's server{} block (mounted at /etc/nginx/conf.d/default.conf,
|
||||
which the base image's own nginx.conf already includes from inside its own
|
||||
http{} block — this template must therefore emit ONLY a server{} block, see
|
||||
compose/config/nginx.*.conf for the same constraint hit empirically).
|
||||
*/}}
|
||||
{{- define "aptly.nginxConf" -}}
|
||||
{{- $sec := include "aptly.security" . | fromJson -}}
|
||||
{{- $p := .Values.proxy -}}
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
client_max_body_size {{ $p.maxUploadSize }};
|
||||
absolute_redirect off;
|
||||
{{- range .Values.security.trustedProxies }}
|
||||
set_real_ip_from {{ . }};
|
||||
{{- end }}
|
||||
{{- if .Values.security.trustedProxies }}
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
{{- end }}
|
||||
|
||||
location = /healthz { access_log off; return 200 "ok\n"; }
|
||||
location = /api/ready { access_log off; proxy_pass http://127.0.0.1:8080; }
|
||||
location = /api/healthy { access_log off; proxy_pass http://127.0.0.1:8080; }
|
||||
|
||||
{{- if $sec.w }}
|
||||
location /api/ {
|
||||
{{- if .Values.security.write.allowCIDRs }}
|
||||
{{- if and .Values.security.write.allowCIDRs (not .Values.security.trustedProxies) }}
|
||||
# WARNING: write.allowCIDRs is set without security.trustedProxies. Behind
|
||||
# an Ingress controller, $remote_addr is the CONTROLLER's pod IP, not the
|
||||
# real client — this will match every client on earth. Set
|
||||
# trustedProxies to the controller's CIDR, or use networkPolicy instead.
|
||||
{{- end }}
|
||||
{{- range .Values.security.write.allowCIDRs }}
|
||||
allow {{ . }};
|
||||
{{- end }}
|
||||
deny all;
|
||||
satisfy {{ if $sec.wa }}any{{ else }}all{{ end }};
|
||||
{{- end }}
|
||||
{{- if $sec.wa }}
|
||||
auth_basic "aptly";
|
||||
auth_basic_user_file /run/aptly/htpasswd;
|
||||
{{- else }}
|
||||
auth_basic off;
|
||||
{{- end }}
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout {{ .Values.proxy.readTimeout }};
|
||||
proxy_send_timeout {{ .Values.proxy.readTimeout }};
|
||||
}
|
||||
{{- else }}
|
||||
location /api/ { return 404; }
|
||||
{{- end }}
|
||||
|
||||
location = /signing-key.asc {
|
||||
alias /run/aptly/pub/signing-key.asc;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
{{- if $sec.r }}
|
||||
location / {
|
||||
{{- if $sec.ra }}
|
||||
auth_basic "aptly";
|
||||
auth_basic_user_file /run/aptly/htpasswd;
|
||||
{{- end }}
|
||||
root /var/lib/aptly/public;
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
|
||||
location ~* /(InRelease|Release|Release\.gpg|Packages(\.[a-z0-9]+)?|Sources(\.[a-z0-9]+)?)$ {
|
||||
{{- if $sec.ra }}
|
||||
auth_basic "aptly";
|
||||
auth_basic_user_file /run/aptly/htpasswd;
|
||||
{{- end }}
|
||||
root /var/lib/aptly/public;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
}
|
||||
{{- if .Values.proxy.compatPaths }}
|
||||
location /repos/{{ .Values.proxy.publishEndpointName }}/ {
|
||||
{{- if $sec.ra }}
|
||||
auth_basic "aptly";
|
||||
auth_basic_user_file /run/aptly/htpasswd;
|
||||
{{- end }}
|
||||
alias /var/lib/aptly/public/;
|
||||
autoindex on;
|
||||
}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
location / { return 404; }
|
||||
{{- end }}
|
||||
}
|
||||
{{- if .Values.metrics.service.enabled }}
|
||||
|
||||
# Separate, unauthenticated listener so scraping never needs the write-path
|
||||
# credentials and a ServiceMonitor never needs a basicAuth secret.
|
||||
server {
|
||||
listen 9090;
|
||||
server_name _;
|
||||
location = /api/metrics { proxy_pass http://127.0.0.1:8080; }
|
||||
location / { return 404; }
|
||||
}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
data:
|
||||
aptly.yaml: |
|
||||
{{- include "aptly.config" . | nindent 4 }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.aptly.gpgKeys }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}-gpg-keys
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range .Values.aptly.gpgKeys }}
|
||||
{{ .name }}.asc: |
|
||||
{{- .armored | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}-nginx
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
data:
|
||||
default.conf: |
|
||||
{{- include "aptly.nginxConf" . | nindent 4 }}
|
||||
@@ -0,0 +1,11 @@
|
||||
{{- if .Values.reconcile.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}-state
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
data:
|
||||
state.yaml: |
|
||||
{{- dict "localRepos" .Values.aptly.localRepos "mirrors" .Values.aptly.mirrors "publish" .Values.aptly.publish | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,65 @@
|
||||
{{- include "aptly.exposureGuard" . -}}
|
||||
{{- if .Values.gateway.enabled }}
|
||||
{{- $fullname := include "aptly.fullname" . -}}
|
||||
{{- $svcName := $fullname -}}
|
||||
{{- if not .Values.gateway.parentRefs }}
|
||||
{{- fail "gateway.enabled=true requires gateway.parentRefs to reference at least one existing Gateway — this chart never creates a Gateway itself." -}}
|
||||
{{- end }}
|
||||
---
|
||||
# Repo HTTPRoute — apt clients. In `single` mode this is also where /api/
|
||||
# traffic arrives; nginx does the read/write split internally (see
|
||||
# templates/_helpers.tpl's aptly.nginxConf). In `split` mode, note that this
|
||||
# is a route-level split only: nginx does not itself reject /api/ requests
|
||||
# that arrive via this hostname, so combine `split` mode with
|
||||
# security.write.inClusterOnly or a NetworkPolicy if you need that enforced.
|
||||
# TLS is configured on the referenced Gateway's listener, not here.
|
||||
apiVersion: {{ .Values.gateway.apiVersion }}
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ $fullname }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- toYaml .Values.gateway.parentRefs | nindent 4 }}
|
||||
{{- with .Values.gateway.repo.hostnames }}
|
||||
hostnames:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: {{ .Values.gateway.repo.pathType }}
|
||||
value: {{ .Values.gateway.repo.path }}
|
||||
backendRefs:
|
||||
- name: {{ $svcName }}
|
||||
port: {{ .Values.service.port }}
|
||||
{{- if and (eq .Values.gateway.mode "split") .Values.gateway.api.enabled (not .Values.security.write.inClusterOnly) }}
|
||||
---
|
||||
# API HTTPRoute (split mode) — a separate hostname/Gateway so you can put a
|
||||
# different Gateway, mTLS, or WAF policy in front of the mutating API than
|
||||
# the public read path gets.
|
||||
apiVersion: {{ .Values.gateway.apiVersion }}
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ $fullname }}-api
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- $apiParentRefs := .Values.gateway.api.parentRefs | default .Values.gateway.parentRefs }}
|
||||
parentRefs:
|
||||
{{- toYaml $apiParentRefs | nindent 4 }}
|
||||
{{- with .Values.gateway.api.hostnames }}
|
||||
hostnames:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: {{ $svcName }}
|
||||
port: {{ .Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,77 @@
|
||||
{{- include "aptly.exposureGuard" . -}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $fullname := include "aptly.fullname" . -}}
|
||||
{{- $svcName := $fullname -}}
|
||||
---
|
||||
# Repo Ingress — apt clients. In `single` mode this is also where /api/
|
||||
# traffic arrives; nginx does the read/write split internally (see
|
||||
# templates/_helpers.tpl's aptly.nginxConf). In `split` mode, note that this
|
||||
# is a DNS/Ingress-level split only: nginx does not itself reject /api/
|
||||
# requests that arrive via this host, so combine `split` mode with
|
||||
# security.write.inClusterOnly or a NetworkPolicy if you need that enforced.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullname }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.repo.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- {{- with .Values.ingress.repo.host }}
|
||||
host: {{ . | quote }}
|
||||
{{- end }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingress.repo.path }}
|
||||
pathType: {{ .Values.ingress.repo.pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ $svcName }}
|
||||
port:
|
||||
name: http
|
||||
{{- if and (eq .Values.ingress.mode "split") .Values.ingress.api.enabled (not .Values.security.write.inClusterOnly) }}
|
||||
---
|
||||
# API Ingress (split mode) — a separate host so you can put a different
|
||||
# ingressClass, mTLS, or WAF policy in front of the mutating API than the
|
||||
# public read path gets.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullname }}-api
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml (merge .Values.ingress.api.annotations .Values.ingress.annotations) | nindent 4 }}
|
||||
spec:
|
||||
{{- $apiClass := .Values.ingress.api.className | default .Values.ingress.className }}
|
||||
{{- with $apiClass }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.api.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- {{- with .Values.ingress.api.host }}
|
||||
host: {{ . | quote }}
|
||||
{{- end }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $svcName }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,80 @@
|
||||
{{- if .Values.reconcile.enabled }}
|
||||
{{- $fullname := include "aptly.fullname" . -}}
|
||||
{{- $img := .Values.reconcile.image -}}
|
||||
{{- $repo := $img.repository | default .Values.image.repository -}}
|
||||
{{- $tag := $img.tag | default .Values.image.tag | default .Chart.AppVersion -}}
|
||||
{{- $registry := .Values.global.imageRegistry -}}
|
||||
{{- $image := ternary (printf "%s/%s:%s" $registry $repo $tag) (printf "%s:%s" $repo $tag) (ne $registry "") -}}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
{{- if eq .Values.reconcile.mode "hook" }}
|
||||
name: {{ $fullname }}-reconcile
|
||||
annotations:
|
||||
helm.sh/hook: post-install,post-upgrade
|
||||
helm.sh/hook-weight: "5"
|
||||
# Deliberately no hook-failed: a failed Job stays around for `kubectl
|
||||
# logs`/`kubectl describe job` instead of vanishing before anyone can
|
||||
# read why reconciliation didn't converge.
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
{{- else }}
|
||||
# A plain (non-hook) Job named after the desired state's hash, for GitOps
|
||||
# controllers (ArgoCD/Flux) that reconcile hooks poorly: it only re-runs
|
||||
# when aptly.{gpgKeys,localRepos,mirrors,publish} actually change.
|
||||
name: {{ $fullname }}-reconcile-{{ dict "localRepos" .Values.aptly.localRepos "mirrors" .Values.aptly.mirrors "publish" .Values.aptly.publish | toYaml | sha256sum | trunc 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: {{ mul .Values.reconcile.timeoutSeconds 2 }}
|
||||
{{- if ne .Values.reconcile.mode "hook" }}
|
||||
ttlSecondsAfterFinished: 86400
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "aptly.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: reconcile
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
{{- include "aptly.imagePullSecrets" . | nindent 6 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: reconcile
|
||||
image: {{ $image }}
|
||||
imagePullPolicy: {{ $img.pullPolicy | default .Values.image.pullPolicy }}
|
||||
command: ["/usr/local/bin/aptly-reconcile"]
|
||||
securityContext:
|
||||
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: APTLY_URL
|
||||
value: "http://{{ $fullname }}:{{ .Values.service.port }}"
|
||||
- name: APTLY_STATE_FILE
|
||||
value: /state.yaml
|
||||
- name: APTLY_FAIL_ON_ERROR
|
||||
value: {{ .Values.reconcile.failOnError | quote }}
|
||||
- name: APTLY_WAIT_TIMEOUT
|
||||
value: {{ .Values.reconcile.timeoutSeconds | quote }}
|
||||
{{- if .Values.security.auth.internalUser.enabled }}
|
||||
- name: APTLY_USER
|
||||
value: {{ .Values.security.auth.internalUser.username | quote }}
|
||||
- name: APTLY_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $fullname }}-credentials
|
||||
key: internal-password
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: state
|
||||
mountPath: /state.yaml
|
||||
subPath: state.yaml
|
||||
readOnly: true
|
||||
resources:
|
||||
{{- toYaml .Values.reconcile.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: state
|
||||
configMap:
|
||||
name: {{ $fullname }}-state
|
||||
{{- end }}
|
||||
@@ -0,0 +1,51 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "aptly.selectorLabels" . | nindent 6 }}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
{{- if .Values.networkPolicy.allowedNamespaces }}
|
||||
# Restricted to these namespaces (plus this one). NOTE: this applies to
|
||||
# the whole nginx:8080 endpoint — read and write share one port, so this
|
||||
# cannot itself express "reads are public, writes are cluster-only" any
|
||||
# more precisely than security.write.allowCIDRs can (see the warning
|
||||
# rendered into nginx.conf for that). Use it to fence the Service off
|
||||
# from unrelated namespaces, not as a read/write split.
|
||||
- from:
|
||||
- podSelector: {}
|
||||
{{- range .Values.networkPolicy.allowedNamespaces }}
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ . }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- {}
|
||||
{{- end }}
|
||||
{{- with .Values.networkPolicy.extraIngress }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
egress:
|
||||
{{- if .Values.networkPolicy.egress.allowAll }}
|
||||
- {}
|
||||
{{- else }}
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
{{- with .Values.networkPolicy.egress.extra }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
spec:
|
||||
# With replicas fixed at 1 (see templates/statefulset.yaml), a
|
||||
# minAvailable:1 budget would block every voluntary node drain forever —
|
||||
# maxUnavailable is the only sane knob here.
|
||||
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "aptly.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{- $fullname := include "aptly.fullname" . -}}
|
||||
{{- $secretName := printf "%s-credentials" $fullname -}}
|
||||
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}}
|
||||
{{- $internalPassword := "" -}}
|
||||
{{- if and $existing (hasKey $existing.data "internal-password") -}}
|
||||
{{- $internalPassword = index $existing.data "internal-password" | b64dec -}}
|
||||
{{- else -}}
|
||||
{{- $internalPassword = randAlphaNum 32 -}}
|
||||
{{- end -}}
|
||||
{{- $userLines := list -}}
|
||||
{{- range $name, $pass := .Values.security.auth.users -}}
|
||||
{{- $userLines = append $userLines (printf "%s:%s" $name $pass) -}}
|
||||
{{- end -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ $secretName }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
# Reused across `helm upgrade` (via `lookup` above) and preserved across
|
||||
# `helm uninstall` so aptly-reconcile keeps working without a manual step
|
||||
# after a reinstall. Never regenerate this key from a template — see
|
||||
# rootfs/usr/local/bin/aptly-init for why hashing must not happen here.
|
||||
helm.sh/resource-policy: keep
|
||||
type: Opaque
|
||||
stringData:
|
||||
internal-password: {{ $internalPassword | quote }}
|
||||
{{- if $userLines }}
|
||||
users: |
|
||||
{{- range $userLines }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{- if and .Values.aptly.gpg.enabled (not .Values.aptly.gpg.signingKey.existingSecret) (.Values.aptly.gpg.signingKey.privateKey) }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}-gpg
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
privateKey: {{ .Values.aptly.gpg.signingKey.privateKey | quote }}
|
||||
{{- if .Values.aptly.gpg.signingKey.passphrase }}
|
||||
passphrase: {{ .Values.aptly.gpg.signingKey.passphrase | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,21 @@
|
||||
{{- if .Values.metrics.service.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}-metrics
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
{{- with .Values.metrics.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "aptly.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: metrics
|
||||
port: {{ .Values.metrics.service.port }}
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "aptly.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
@@ -0,0 +1,23 @@
|
||||
{{- if and .Values.metrics.serviceMonitor.enabled .Values.metrics.service.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "aptly.fullname" . }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
{{- with .Values.metrics.serviceMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "aptly.selectorLabels" . | nindent 6 }}
|
||||
endpoints:
|
||||
- port: metrics
|
||||
path: /api/metrics
|
||||
interval: {{ .Values.metrics.serviceMonitor.interval }}
|
||||
{{- with .Values.metrics.serviceMonitor.relabelings }}
|
||||
relabelings:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,298 @@
|
||||
{{- $fullname := include "aptly.fullname" . -}}
|
||||
{{- $hasGpgSecret := or .Values.aptly.gpg.signingKey.existingSecret (and (not .Values.aptly.gpg.signingKey.existingSecret) .Values.aptly.gpg.signingKey.privateKey) -}}
|
||||
{{- $gpgSecretName := .Values.aptly.gpg.signingKey.existingSecret | default (printf "%s-gpg" $fullname) -}}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ $fullname }}
|
||||
labels:
|
||||
{{- include "aptly.labels" . | nindent 4 }}
|
||||
{{- with .Values.workload.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceName: {{ $fullname }}
|
||||
# LevelDB (aptly's database) takes an exclusive OS-level file lock — two
|
||||
# writers would corrupt it. A StatefulSet with replicas=1 always terminates
|
||||
# the old pod before creating its replacement, so RollingUpdate is safe
|
||||
# here in a way it would not be for a Deployment on a ReadWriteOnce PVC
|
||||
# (which would deadlock on a Multi-Attach error instead).
|
||||
replicas: 1
|
||||
podManagementPolicy: {{ .Values.workload.podManagementPolicy }}
|
||||
revisionHistoryLimit: {{ .Values.workload.revisionHistoryLimit }}
|
||||
updateStrategy:
|
||||
type: {{ .Values.workload.updateStrategy.type }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "aptly.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "aptly.selectorLabels" . | nindent 8 }}
|
||||
{{- with .Values.workload.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
checksum/config: {{ include "aptly.config" . | sha256sum }}
|
||||
checksum/nginx: {{ include "aptly.nginxConf" . | sha256sum }}
|
||||
{{- with .Values.workload.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- include "aptly.imagePullSecrets" . | nindent 6 }}
|
||||
terminationGracePeriodSeconds: {{ .Values.workload.terminationGracePeriodSeconds }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.priorityClassName }}
|
||||
priorityClassName: {{ . }}
|
||||
{{- end }}
|
||||
initContainers:
|
||||
- name: config-init
|
||||
image: {{ include "aptly.image" . }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command: ["/usr/local/bin/aptly-init"]
|
||||
securityContext:
|
||||
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: APTLY_ROOT_DIR
|
||||
value: /var/lib/aptly
|
||||
- name: APTLY_CONFIG_SRC
|
||||
value: /etc/aptly-src/aptly.yaml
|
||||
- name: APTLY_CONFIG_DST
|
||||
value: /run/aptly/aptly.yaml
|
||||
- name: APTLY_GPG_ENABLED
|
||||
value: {{ .Values.aptly.gpg.enabled | quote }}
|
||||
{{- if not .Values.security.auth.existingSecret }}
|
||||
- name: APTLY_USERS_FILE
|
||||
value: /etc/aptly-secrets/users
|
||||
{{- else }}
|
||||
- name: APTLY_HTPASSWD_SRC
|
||||
value: /etc/aptly-secrets-existing/htpasswd
|
||||
{{- end }}
|
||||
{{- if .Values.security.auth.internalUser.enabled }}
|
||||
- name: APTLY_INTERNAL_USER
|
||||
value: {{ .Values.security.auth.internalUser.username | quote }}
|
||||
- name: APTLY_INTERNAL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $fullname }}-credentials
|
||||
key: internal-password
|
||||
{{- end }}
|
||||
{{- if $hasGpgSecret }}
|
||||
- name: APTLY_GPG_PRIVATE_KEY_FILE
|
||||
value: /etc/aptly-gpg/privateKey
|
||||
- name: APTLY_GPG_SECRET_KEYRING_FILE
|
||||
value: /etc/aptly-gpg/secretKeyring
|
||||
- name: APTLY_GPG_PASSPHRASE_FILE
|
||||
value: /etc/aptly-gpg/passphrase
|
||||
{{- end }}
|
||||
{{- if .Values.aptly.gpgKeys }}
|
||||
- name: APTLY_GPG_KEYS_DIR
|
||||
value: /etc/aptly-gpg-keys
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: config-src
|
||||
mountPath: /etc/aptly-src
|
||||
readOnly: true
|
||||
- name: run
|
||||
mountPath: /run/aptly
|
||||
- name: data
|
||||
mountPath: /var/lib/aptly
|
||||
{{- if not .Values.security.auth.existingSecret }}
|
||||
- name: credentials
|
||||
mountPath: /etc/aptly-secrets
|
||||
readOnly: true
|
||||
{{- else }}
|
||||
- name: credentials-existing
|
||||
mountPath: /etc/aptly-secrets-existing
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if $hasGpgSecret }}
|
||||
- name: gpg-secret
|
||||
mountPath: /etc/aptly-gpg
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if .Values.aptly.gpgKeys }}
|
||||
- name: gpg-keys
|
||||
mountPath: /etc/aptly-gpg-keys
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.extraInitContainers }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: aptly
|
||||
image: {{ include "aptly.image" . }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: APTLY_API_LISTEN
|
||||
value: {{ include "aptly.apiListen" . }}
|
||||
- name: APTLY_CONFIG
|
||||
value: /run/aptly/aptly.yaml
|
||||
- name: GNUPGHOME
|
||||
value: /run/aptly/gnupg
|
||||
{{- with .Values.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if or .Values.aptly.existingSecretEnv .Values.extraEnvFrom }}
|
||||
envFrom:
|
||||
{{- range .Values.aptly.existingSecretEnv }}
|
||||
- secretRef:
|
||||
name: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraEnvFrom }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: aptly
|
||||
containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/aptly
|
||||
- name: run
|
||||
mountPath: /run/aptly
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
{{- with .Values.extraVolumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
startupProbe:
|
||||
httpGet: { path: /api/ready, port: aptly }
|
||||
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
|
||||
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet: { path: /api/ready, port: aptly }
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
livenessProbe:
|
||||
httpGet: { path: /api/healthy, port: aptly }
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
- name: nginx
|
||||
image: {{ include "aptly.nginxImage" . }}
|
||||
imagePullPolicy: {{ .Values.nginx.image.pullPolicy }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.nginx.securityContext | nindent 12 }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
{{- if .Values.metrics.service.enabled }}
|
||||
- name: metrics
|
||||
containerPort: 9090
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/aptly/public
|
||||
subPath: public
|
||||
readOnly: true
|
||||
- name: run
|
||||
mountPath: /run/aptly
|
||||
readOnly: true
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: nginx-cache
|
||||
mountPath: /var/cache/nginx
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: http }
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: http }
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
{{- toYaml .Values.nginx.resources | nindent 12 }}
|
||||
{{- with .Values.extraContainers }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: config-src
|
||||
configMap:
|
||||
name: {{ $fullname }}-config
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: {{ $fullname }}-nginx
|
||||
- name: run
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 16Mi
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: nginx-cache
|
||||
emptyDir: {}
|
||||
{{- if not .Values.security.auth.existingSecret }}
|
||||
- name: credentials
|
||||
secret:
|
||||
secretName: {{ $fullname }}-credentials
|
||||
optional: true
|
||||
{{- else }}
|
||||
- name: credentials-existing
|
||||
secret:
|
||||
secretName: {{ .Values.security.auth.existingSecret }}
|
||||
{{- end }}
|
||||
{{- if $hasGpgSecret }}
|
||||
- name: gpg-secret
|
||||
secret:
|
||||
secretName: {{ $gpgSecretName }}
|
||||
optional: true
|
||||
{{- end }}
|
||||
{{- if .Values.aptly.gpgKeys }}
|
||||
- name: gpg-keys
|
||||
configMap:
|
||||
name: {{ $fullname }}-gpg-keys
|
||||
{{- end }}
|
||||
{{- if not .Values.persistence.enabled }}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- else if .Values.persistence.existingClaim }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.existingClaim }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
{{- with .Values.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes: [{{ .Values.persistence.accessMode }}]
|
||||
{{- $sc := .Values.persistence.storageClass | default .Values.global.defaultStorageClass }}
|
||||
{{- if $sc }}
|
||||
storageClassName: {{ $sc }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,421 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "aptly",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nameOverride": { "type": "string" },
|
||||
"fullnameOverride": { "type": "string" },
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"repository": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] },
|
||||
"pullSecrets": { "type": "array" }
|
||||
}
|
||||
},
|
||||
"nginx": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"repository": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
|
||||
}
|
||||
},
|
||||
"resources": { "type": "object" },
|
||||
"securityContext": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"aptly": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"architectures": { "type": "array", "items": { "type": "string" } },
|
||||
"logLevel": { "type": "string", "enum": ["debug", "info", "warn", "error"] },
|
||||
"logFormat": { "type": "string", "enum": ["default", "json"] },
|
||||
"download": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"concurrency": { "type": "integer", "minimum": 1 },
|
||||
"limit": { "type": "integer", "minimum": 0 },
|
||||
"retries": { "type": "integer", "minimum": 0 },
|
||||
"sourcePackages": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"publishing": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"skipContents": { "type": "boolean" },
|
||||
"skipBz2": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": { "enabled": { "type": "boolean" } }
|
||||
},
|
||||
"swagger": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": { "enabled": { "type": "boolean" } }
|
||||
},
|
||||
"gpg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"verify": { "type": "boolean" },
|
||||
"provider": { "type": "string", "enum": ["gpg", "internal"] },
|
||||
"signingKey": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"existingSecret": { "type": "string" },
|
||||
"privateKey": { "type": "string" },
|
||||
"passphrase": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"publishPublicKey": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gpgKeys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "armored"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"armored": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"localRepos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"comment": { "type": "string" },
|
||||
"defaultDistribution": { "type": "string" },
|
||||
"defaultComponent": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"mirrors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "archiveURL", "distribution"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"archiveURL": { "type": "string" },
|
||||
"distribution": { "type": "string" },
|
||||
"components": { "type": "array", "items": { "type": "string" } },
|
||||
"architectures": { "type": "array", "items": { "type": "string" } },
|
||||
"filter": { "type": "string" },
|
||||
"filterWithDeps": { "type": "boolean" },
|
||||
"downloadSources": { "type": "boolean" },
|
||||
"downloadUdebs": { "type": "boolean" },
|
||||
"downloadInstaller": { "type": "boolean" },
|
||||
"downloadAppStream": { "type": "boolean" },
|
||||
"ignoreSignatures": { "type": "boolean" },
|
||||
"keyrings": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "distribution"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"prefix": { "type": "string" },
|
||||
"distribution": { "type": "string" },
|
||||
"sourceKind": { "type": "string", "enum": ["local", "snapshot"] },
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "component"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"component": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"architectures": { "type": "array", "items": { "type": "string" } },
|
||||
"acquireByHash": { "type": "boolean" },
|
||||
"skipContents": { "type": "boolean" },
|
||||
"skipBz2": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"configOverrides": { "type": "object", "additionalProperties": true },
|
||||
"existingSecretEnv": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"preset": { "type": "string", "enum": ["open", "publicRead", "authenticated", "readOnly"] },
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"users": { "type": "object", "additionalProperties": { "type": "string" } },
|
||||
"existingSecret": { "type": "string" },
|
||||
"internalUser": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"username": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trustedProxies": { "type": "array", "items": { "type": "string" } },
|
||||
"read": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"requireAuth": { "type": ["boolean", "null"] },
|
||||
"allowCIDRs": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"requireAuth": { "type": ["boolean", "null"] },
|
||||
"allowCIDRs": { "type": "array", "items": { "type": "string" } },
|
||||
"inClusterOnly": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"compatPaths": { "type": "boolean" },
|
||||
"maxUploadSize": { "type": "string" },
|
||||
"readTimeout": { "type": "string" },
|
||||
"publishEndpointName": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"persistence": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"existingClaim": { "type": "string" },
|
||||
"storageClass": { "type": "string" },
|
||||
"accessMode": { "type": "string", "enum": ["ReadWriteOnce", "ReadWriteOncePod"] },
|
||||
"size": { "type": "string" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"workload": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"updateStrategy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": { "type": { "type": "string", "enum": ["RollingUpdate", "OnDelete"] } }
|
||||
},
|
||||
"podManagementPolicy": { "type": "string", "enum": ["OrderedReady", "Parallel"] },
|
||||
"revisionHistoryLimit": { "type": "integer", "minimum": 0 },
|
||||
"terminationGracePeriodSeconds": { "type": "integer", "minimum": 0 },
|
||||
"annotations": { "type": "object" },
|
||||
"podAnnotations": { "type": "object" },
|
||||
"podLabels": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"podSecurityContext": { "type": "object" },
|
||||
"containerSecurityContext": { "type": "object" },
|
||||
"resources": { "type": "object" },
|
||||
"probes": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"startup": { "type": "object" },
|
||||
"readiness": { "type": "object" },
|
||||
"liveness": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
|
||||
"port": { "type": "integer" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"mode": { "type": "string", "enum": ["single", "split"] },
|
||||
"className": { "type": "string" },
|
||||
"annotations": { "type": "object", "additionalProperties": true },
|
||||
"repo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"host": { "type": "string" },
|
||||
"path": { "type": "string" },
|
||||
"pathType": { "type": "string", "enum": ["Prefix", "Exact", "ImplementationSpecific"] },
|
||||
"tls": { "type": "array" }
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"host": { "type": "string" },
|
||||
"className": { "type": "string" },
|
||||
"annotations": { "type": "object", "additionalProperties": true },
|
||||
"tls": { "type": "array" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gateway": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"apiVersion": { "type": "string" },
|
||||
"mode": { "type": "string", "enum": ["single", "split"] },
|
||||
"parentRefs": { "type": "array" },
|
||||
"repo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"hostnames": { "type": "array", "items": { "type": "string" } },
|
||||
"path": { "type": "string" },
|
||||
"pathType": { "type": "string", "enum": ["PathPrefix", "Exact", "RegularExpression"] }
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"hostnames": { "type": "array", "items": { "type": "string" } },
|
||||
"parentRefs": { "type": "array" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"service": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"port": { "type": "integer" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"serviceMonitor": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"interval": { "type": "string" },
|
||||
"labels": { "type": "object" },
|
||||
"relabelings": { "type": "array" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"reconcile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"mode": { "type": "string", "enum": ["hook", "job", "manual"] },
|
||||
"failOnError": { "type": "boolean" },
|
||||
"timeoutSeconds": { "type": "integer", "minimum": 1 },
|
||||
"image": { "type": "object" },
|
||||
"resources": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"podDisruptionBudget": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"maxUnavailable": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
},
|
||||
"networkPolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"allowedNamespaces": { "type": "array", "items": { "type": "string" } },
|
||||
"extraIngress": { "type": "array" },
|
||||
"egress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowAll": { "type": "boolean" },
|
||||
"extra": { "type": "array" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extraEnv": { "type": "array" },
|
||||
"extraEnvFrom": { "type": "array" },
|
||||
"extraVolumes": { "type": "array" },
|
||||
"extraVolumeMounts": { "type": "array" },
|
||||
"extraInitContainers": { "type": "array" },
|
||||
"extraContainers": { "type": "array" },
|
||||
"nodeSelector": { "type": "object" },
|
||||
"tolerations": { "type": "array" },
|
||||
"affinity": { "type": "object" },
|
||||
"topologySpreadConstraints": { "type": "array" },
|
||||
"priorityClassName": { "type": "string" },
|
||||
"global": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"imageRegistry": { "type": "string" },
|
||||
"imagePullSecrets": { "type": "array" },
|
||||
"defaultStorageClass": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# -- Container image for the aptly server itself (also used for the
|
||||
# initContainer and the reconcile Job — all three run the same image).
|
||||
image:
|
||||
repository: git.morlana.online/f.weber/aptly
|
||||
tag: "" # "" -> .Chart.AppVersion, i.e. the last released image. Never "latest".
|
||||
pullPolicy: IfNotPresent
|
||||
pullSecrets: []
|
||||
|
||||
# -- The read/auth sidecar. A plain upstream image — this chart owns none of
|
||||
# its code, only its rendered config (see `security` below).
|
||||
nginx:
|
||||
image:
|
||||
repository: nginxinc/nginx-unprivileged
|
||||
tag: "1-alpine"
|
||||
pullPolicy: IfNotPresent
|
||||
resources: {}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities: { drop: [ALL] }
|
||||
|
||||
# =============================================================================
|
||||
# aptly configuration. Two layers, always merged in this order:
|
||||
# 1. the curated keys below (omitted from the rendered config when unset,
|
||||
# so a default install matches aptly's own upstream defaults exactly)
|
||||
# 2. aptly.configOverrides — raw aptly YAML, deep-merged last, always wins.
|
||||
# Every current and future aptly config key is reachable here without a
|
||||
# chart change. See rootfs/usr/local/bin/aptly-init and
|
||||
# https://github.com/aptly-dev/aptly/blob/master/utils/config.go for the
|
||||
# full field list (snake_case yaml tags).
|
||||
# =============================================================================
|
||||
aptly:
|
||||
architectures: []
|
||||
logLevel: info
|
||||
logFormat: json
|
||||
download:
|
||||
concurrency: 4
|
||||
limit: 0
|
||||
retries: 0
|
||||
sourcePackages: false
|
||||
publishing:
|
||||
skipContents: false
|
||||
skipBz2: false
|
||||
|
||||
metrics:
|
||||
enabled: false
|
||||
swagger:
|
||||
enabled: false
|
||||
|
||||
gpg:
|
||||
# false -> gpg_disable_sign: true AND Signing.Skip: true on every publish
|
||||
# call the reconcile Job makes (both are required — aptly's publish API
|
||||
# does not consult gpg_disable_sign on its own, see docs/security.md).
|
||||
enabled: true
|
||||
verify: true
|
||||
# gpg (default): the real gnupg binary, in the image already — handles
|
||||
# armored keys, subkeys and passphrases the way upstream aptly expects.
|
||||
# internal: pure-Go openpgp, no gnupg binary needed, smaller attack
|
||||
# surface — verify your key type works with it before switching.
|
||||
provider: gpg
|
||||
signingKey:
|
||||
# Secret with key "privateKey" (armored .asc) or "secretKeyring"
|
||||
# (binary secring.gpg), optionally "passphrase". Never put a real key
|
||||
# inline in values.yaml — this is the production path.
|
||||
existingSecret: ""
|
||||
# Discouraged escape hatch for quick tests only.
|
||||
privateKey: ""
|
||||
passphrase: ""
|
||||
publishPublicKey:
|
||||
enabled: true
|
||||
path: /signing-key.asc
|
||||
|
||||
# Trusted keys imported into GNUPGHOME on every start, for mirror
|
||||
# signature verification. The keyring is therefore a pure function of
|
||||
# values.yaml — restart the pod to pick up an edit here.
|
||||
# Inline ASCII-armored public keys only (Helm has no network access at
|
||||
# render time to support a `url:`/`keyserver:` form the way a plain script
|
||||
# could — fetch the key yourself once and paste it here).
|
||||
gpgKeys: []
|
||||
# - name: debian-archive
|
||||
# armored: |
|
||||
# -----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
# ...
|
||||
|
||||
# Declarative desired state, reconciled by the post-install/post-upgrade
|
||||
# Job against the REST API (never the CLI — see reconcile.mode below and
|
||||
# rootfs/usr/local/bin/aptly-reconcile for the exact field reference and
|
||||
# the documented limitation on editing mirrors[].components after creation).
|
||||
localRepos: []
|
||||
# - name: stable
|
||||
# comment: "Production package repository"
|
||||
# defaultDistribution: stable
|
||||
# defaultComponent: main
|
||||
|
||||
mirrors: []
|
||||
# - name: debian-security
|
||||
# archiveURL: http://security.debian.org/debian-security
|
||||
# distribution: trixie-security
|
||||
# components: [main]
|
||||
# architectures: [amd64, arm64]
|
||||
|
||||
publish: []
|
||||
# - name: stable-root
|
||||
# prefix: "" # "" = repo root
|
||||
# distribution: stable
|
||||
# sourceKind: local # local | snapshot
|
||||
# sources: [{ name: stable, component: main }]
|
||||
# architectures: [amd64, arm64]
|
||||
# acquireByHash: true
|
||||
|
||||
# Raw passthrough, deep-merged over the generated config last. See header.
|
||||
configOverrides: {}
|
||||
|
||||
# Existing Secrets to envFrom into the aptly container, so configOverrides
|
||||
# can reference ${VAR} placeholders (e.g. S3 credentials) that resolve from
|
||||
# Secrets you already manage, without ever putting them in values.yaml.
|
||||
existingSecretEnv: []
|
||||
|
||||
# =============================================================================
|
||||
# Security matrix — one preset switch, escape hatches for every axis. See
|
||||
# docs/security.md for the full decision table.
|
||||
# =============================================================================
|
||||
security:
|
||||
# open: read+write, no auth, no exceptions — the explicit "unabgesichert"
|
||||
# mode. publicRead (default): read is open, write needs Basic Auth.
|
||||
# authenticated: both need Basic Auth. readOnly: write returns 404.
|
||||
preset: publicRead
|
||||
auth:
|
||||
# name: plaintext password. Hashed into htpasswd by the initContainer at
|
||||
# pod start (never a bcrypt/apr1 hash here — see rootfs/.../aptly-init
|
||||
# for why: a template-side hash would change, hence restart-loop, on
|
||||
# every single helm upgrade).
|
||||
users: {}
|
||||
# Secret key "htpasswd" (pre-hashed) — the recommended production path,
|
||||
# e.g. via ExternalSecrets/SealedSecrets. Wins over `users` when set.
|
||||
existingSecret: ""
|
||||
internalUser:
|
||||
# Always appended to htpasswd: the reconcile Job talks to nginx (not
|
||||
# directly to aptly, which is loopback-only), so it needs credentials
|
||||
# in every preset, including existingSecret + authenticated.
|
||||
enabled: true
|
||||
username: aptly-internal
|
||||
# CIDRs matched against $remote_addr. Behind an Ingress controller that is
|
||||
# the CONTROLLER's pod IP, not the real client — set trustedProxies to the
|
||||
# controller's CIDR (via X-Forwarded-For) or use networkPolicy instead. The
|
||||
# chart renders a warning comment into nginx.conf when allowCIDRs is set
|
||||
# without trustedProxies.
|
||||
trustedProxies: []
|
||||
read:
|
||||
enabled: true
|
||||
requireAuth: null # null = take the preset's value; true/false overrides it
|
||||
allowCIDRs: []
|
||||
write:
|
||||
enabled: true
|
||||
requireAuth: null
|
||||
allowCIDRs: []
|
||||
# true: do not render the API Ingress at all (regardless of ingress.api.*)
|
||||
# and rely on networkPolicy for isolation — an honest implementation, not
|
||||
# an nginx trick.
|
||||
inClusterOnly: false
|
||||
|
||||
# proxy.enabled=false hands aptly's unauthenticated write API directly to
|
||||
# whatever can reach the Service — the chart refuses to render an Ingress in
|
||||
# that combination unless security.preset is explicitly "open" (see
|
||||
# templates/NOTES.txt / the `fail` guard in templates/_helpers.tpl).
|
||||
proxy:
|
||||
enabled: true
|
||||
# nginx additionally serves the same tree under /repos/<name>/, matching
|
||||
# aptly's own serve_in_api_mode URL shape, so toggling this flag never
|
||||
# breaks an already-deployed sources.list.
|
||||
compatPaths: true
|
||||
maxUploadSize: "0" # nginx client_max_body_size; "0" = unlimited
|
||||
readTimeout: "3600s"
|
||||
publishEndpointName: public
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
existingClaim: "" # set this in production — see docs/operations.md
|
||||
storageClass: ""
|
||||
accessMode: ReadWriteOnce
|
||||
size: 20Gi
|
||||
annotations: {}
|
||||
|
||||
workload:
|
||||
updateStrategy:
|
||||
type: RollingUpdate # safe here: a StatefulSet with replicas=1 always
|
||||
# terminates the old pod before creating the new one
|
||||
podManagementPolicy: OrderedReady
|
||||
revisionHistoryLimit: 3
|
||||
terminationGracePeriodSeconds: 60
|
||||
annotations: {}
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
fsGroup: 10001
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
seccompProfile: { type: RuntimeDefault }
|
||||
|
||||
containerSecurityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities: { drop: [ALL] }
|
||||
|
||||
resources: {}
|
||||
|
||||
probes:
|
||||
startup: { periodSeconds: 5, failureThreshold: 60 }
|
||||
readiness: { periodSeconds: 10, timeoutSeconds: 3, failureThreshold: 3 }
|
||||
liveness: { periodSeconds: 30, timeoutSeconds: 5, failureThreshold: 6 }
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
annotations: {}
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
mode: single # single | split — see docs/security.md
|
||||
className: ""
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
repo:
|
||||
host: ""
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - hosts: [apt.example.com]
|
||||
# secretName: apt-tls
|
||||
api:
|
||||
enabled: false
|
||||
host: ""
|
||||
className: ""
|
||||
annotations: {}
|
||||
tls: []
|
||||
|
||||
# Gateway API HTTPRoute support — fully independent of `ingress.*` above, and
|
||||
# safe to enable at the same time as it (e.g. mid-migration between the two:
|
||||
# both can point at the same Service simultaneously, see docs/security.md).
|
||||
# This chart never creates a Gateway itself, only HTTPRoutes attaching to one
|
||||
# your cluster admin already manages — TLS is that Gateway listener's job, not
|
||||
# something set here.
|
||||
gateway:
|
||||
enabled: false
|
||||
# Core Gateway API resources are apiVersion gateway.networking.k8s.io/v1 (GA
|
||||
# since v1.0) — override only if your cluster's CRDs are still pre-GA.
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
mode: single # single | split — same meaning as ingress.mode, see docs/security.md
|
||||
# Referenced Gateway(s). Required when gateway.enabled is true.
|
||||
parentRefs: []
|
||||
# - name: my-gateway
|
||||
# namespace: gateway-infra # optional, defaults to this release's namespace
|
||||
# sectionName: https # optional, binds to one named listener
|
||||
repo:
|
||||
hostnames: [] # e.g. [apt.example.com] — omit to match the Gateway listener's own hostname(s)
|
||||
path: /
|
||||
pathType: PathPrefix # PathPrefix | Exact | RegularExpression — Gateway API's own enum, distinct from ingress.repo.pathType's
|
||||
api:
|
||||
enabled: false
|
||||
hostnames: []
|
||||
parentRefs: [] # override for the API route only — falls back to gateway.parentRefs when empty
|
||||
|
||||
metrics:
|
||||
service:
|
||||
enabled: false
|
||||
port: 9090
|
||||
annotations: {}
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
interval: 30s
|
||||
labels: {}
|
||||
relabelings: []
|
||||
|
||||
reconcile:
|
||||
enabled: true
|
||||
# hook (default): post-install,post-upgrade Helm hook Job.
|
||||
# job: a plain Job named with a hash of the desired state, for GitOps
|
||||
# controllers (ArgoCD/Flux) that dislike Helm hooks.
|
||||
# manual: render the state ConfigMap only.
|
||||
mode: hook
|
||||
failOnError: false
|
||||
timeoutSeconds: 600
|
||||
image: {} # override repository/tag/pullPolicy; defaults to the main `image`
|
||||
resources: {}
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
maxUnavailable: 1
|
||||
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
allowedNamespaces: []
|
||||
extraIngress: []
|
||||
egress:
|
||||
# A default-deny egress policy silently breaks every mirror — this stays
|
||||
# true until you have a specific reason to lock it down.
|
||||
allowAll: true
|
||||
extra: []
|
||||
|
||||
extraEnv: []
|
||||
extraEnvFrom: []
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
extraInitContainers: []
|
||||
extraContainers: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
priorityClassName: ""
|
||||
|
||||
global:
|
||||
imageRegistry: ""
|
||||
imagePullSecrets: []
|
||||
defaultStorageClass: ""
|
||||
Reference in New Issue
Block a user