Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0d78376
feat: add remote package development foundations
patrickleet Sep 7, 2026
2433fe3
style: format native registry protocol test
patrickleet Sep 7, 2026
9f453e2
feat(local): one machine cluster up/init/env/tui
patrickleet Sep 9, 2026
1ec7c81
fix(local): unique env catalog ids and \$HOME mountRoot
patrickleet Sep 10, 2026
bd3d95e
feat(local): embed standard cluster template and overlay on up
patrickleet Sep 11, 2026
1186732
fix(local): keep AuthStack on cluster, drop Harmony identity embed
patrickleet Sep 11, 2026
b0f0e7f
fix(local): AuthStack Service zitadel in namespace auth
patrickleet Sep 12, 2026
315464a
fix(local): do not crawl \$HOME for Environments on up
patrickleet Sep 13, 2026
ac4889b
fix(local): resolve extra Environment yamls from checkout root
patrickleet Sep 15, 2026
93b447e
fix(local): apply k8s env docs as JSON
patrickleet Sep 15, 2026
3a7850f
feat(local): hops local status --urls
patrickleet Sep 15, 2026
e9c9d0f
fix(local): status lists only running workspaces
patrickleet Sep 15, 2026
78a3a28
feat(local): rename tui/dns and compact env + cluster status
patrickleet Sep 15, 2026
c227ead
fix(local): drop tui and dns command aliases
patrickleet Sep 15, 2026
abf5660
feat(local): show cluster hostPath on status
patrickleet Sep 15, 2026
b9204b4
feat(local): configure hostPath on init and via hops local configure
patrickleet Sep 15, 2026
403ab28
fix(local): AuthStack first org is hops, not harmony-local
patrickleet Sep 15, 2026
3f65873
feat(local): Environment.spec.secretSync before deploys
patrickleet Sep 16, 2026
ed04073
fix(local): vault secretSync uses the Environment checkout
patrickleet Sep 16, 2026
af07702
fix(local): secretSync reads Vault root token from vault-0
patrickleet Sep 16, 2026
8518adf
fix(local): register auth.gitkb.localhost on cluster ingress
patrickleet Sep 16, 2026
978f862
fix(local): ignore immutable Job applies during env reconcile
patrickleet Sep 16, 2026
1424ed0
feat(local): Environment.spec.setup runs on env enable
patrickleet Sep 16, 2026
cd756d3
fix(local): adopt ureq 3 API in package_dev registry client
patrickleet Sep 18, 2026
ff037e6
fix(local): keep hops local up as a first-class command
patrickleet Sep 18, 2026
37ebf84
fix(local): align status and fwd tests with machine-cluster UX
patrickleet Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bootstrap/providers/helm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ kind: Provider
metadata:
name: crossplane-contrib-provider-helm
spec:
package: xpkg.crossplane.io/crossplane-contrib/provider-helm:v1.1.0
package: xpkg.crossplane.io/crossplane-contrib/provider-helm:v1.3.0
runtimeConfigRef:
name: local-dev-helm
6 changes: 6 additions & 0 deletions charts/oci-registry/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v2
name: oci-registry
description: Private Crossplane development artifacts, uploaded through the Kubernetes API
type: application
version: 0.1.0
appVersion: "2.8.3"
95 changes: 95 additions & 0 deletions charts/oci-registry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Crossplane development registry

One Distribution chart for local and remote control planes. This is artifact
storage, not an application deployment framework or a Crossplane XR.

The single pod has two Distribution processes over one content store:

- `registry-upload:5001` selects a **loopback-only** HTTP writer. Reach it using
`kubectl --context CONTEXT -n NAMESPACE port-forward --address 127.0.0.1 service/registry-upload :5001`.
A host-side OCI client uses the printed local port. Docker's remote daemon
must not be used to push to this workstation loopback address.
- `registry:5000` serves HTTPS in Distribution read-only mode. Both Crossplane
and node container runtimes must resolve this name and trust its certificate.
The reader disables redirects, deletion, upload purging, and proxy caching.

The upload Service deliberately has no reachable pod-IP write listener.
Kubelet port-forward connects inside the selected pod network namespace.
NetworkPolicy additionally permits only declared read peers on port 5000.
No NodePort, LoadBalancer, Ingress, DNS, or Gateway resource is created.
Do not add a public route as a workaround for node DNS/trust failures.

A StatefulSet keeps the upload RBAC bound to one stable pod name. Upload
subjects can read pod metadata in this namespace but can port-forward only
`registry-0`; they receive no exec, Secret, or package-manager write rights.
Use a dedicated namespace. Existing cluster-wide RBAC grants are additive and
cannot be revoked by this chart. NetworkPolicy needs an enforcing CNI and does
not isolate a compromised node or cluster administrator.

## Inputs and rendering

Provide an existing TLS Secret, explicit allowed read peers (including runtime
node CIDRs), and encrypted storage. The chart creates no certificates or cloud
infrastructure and accepts no static S3 credentials.

```sh
helm lint charts/oci-registry -f charts/oci-registry/ci/local.yaml
helm template registry charts/oci-registry -n crossplane-dev \
-f charts/oci-registry/ci/remote.yaml
cargo test --test oci_registry_chart
```

The optional native protocol fixture uses the same rendered Distribution
configuration, with temporary filesystem paths, loopback ports, and a test CA:

```sh
HOPS_DISTRIBUTION_BINARY=/absolute/path/to/registry \
cargo test --test distribution_protocol -- --ignored
```

Build that binary from the pinned Distribution v2.8.3 source. This verifies
upload/digest readback, TLS trust failure/success, read-side mutation rejection,
and process-restart durability without Docker or a local control plane. It does
not substitute for Kubernetes RBAC, NetworkPolicy, node pulls, PVC replacement,
or S3/workload-identity testing.

The files in `ci/` are **render fixtures**, not deployment-ready values: replace
documentation CIDRs, sample bucket/IAM names, and TLS Secret names. PVC
encryption is an operator attestation, not something Helm can verify. S3 uses
server-side encryption and HTTPS with existing workload identity; bucket
policies, isolation, and node trust remain infrastructure responsibilities.
A cloud credential plugin already used by kubeconfig is not a new Hops AWS
authentication requirement.

An Argo Application can select `charts/oci-registry` from this repository at a
pinned revision. The chart can also be rendered by a local controller or Helm;
Argo itself is not required. The chart and its runtime image must remain
fetchable without this development registry.

## Durability and cleanup

There is no automatic garbage collector or raw object TTL. Deletion and upload
purging are disabled pending the session-aware reachability/retention workflow.
Do not enable TTL on the bucket prefix. Retain every active and restorable
manifest and its transitive blob closure. Offline Distribution GC must only run
after stopping uploads and establishing that complete retention set; this chart
does not yet automate or claim that proof. PVCs have Helm and Argo retention
annotations, but other inventory controllers must separately honor retention.

## Local adoption boundary

This chart does **not yet replace** the embedded local NodePort installer.
Migrating a live local registry also needs host-side upload transport and node
trust integration. Do not run both writers on the existing claim or apply the
new Service over the old Deployment.

The intended migration reuses `registry-pvc`, the existing TLS Secret, and the
pull hostname, with the old writer stopped first and its manifests retained
for rollback. `storage.pvc.existingClaim` avoids creating/adopting the PVC.
Snapshot/backup the data and verify digest pulls before and after migration.
Neither that migration nor a shared-cluster deployment is performed by render
tests. Registry replacement, strict-TLS node pulls, RBAC denial, and
NetworkPolicy enforcement need the disposable Kubernetes integration fixture.

Configuration references: [Distribution configuration](https://distribution.github.io/distribution/about/configuration/),
[S3 driver](https://distribution.github.io/distribution/storage-drivers/s3/).
17 changes: 17 additions & 0 deletions charts/oci-registry/ci/local.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Render fixture: encryption must be verified on the actual laptop storage.
tls:
existingSecret: hops-local-registry-tls
storage:
pvc:
encryptionConfirmed: true
access:
uploadSubjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: crossplane-developers
pullPeers:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: crossplane-system
- ipBlock:
cidr: 192.0.2.0/24
24 changes: 24 additions & 0 deletions charts/oci-registry/ci/remote.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Render fixture only: replace documentation CIDRs and existing infra bindings.
tls:
existingSecret: development-registry-tls
storage:
type: s3
s3:
bucket: example-existing-development-artifacts
region: us-east-2
kmsKeyId: alias/development-artifacts
serviceAccount:
automountServiceAccountToken: true
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/development-registry
access:
uploadSubjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: crossplane-developers
pullPeers:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: crossplane-system
- ipBlock:
cidr: 192.0.2.0/24
32 changes: 32 additions & 0 deletions charts/oci-registry/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{- define "oci.labels" -}}
app.kubernetes.io/name: oci-registry
app.kubernetes.io/instance: {{ .Release.Name }}
hops.ops.com.ai/registry-mode: push
{{- end -}}

{{- define "oci.selector" -}}
app.kubernetes.io/name: oci-registry
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

{{- define "oci.storage" -}}
{{- if eq .Values.storage.type "pvc" }}
filesystem:
rootdirectory: /var/lib/registry
{{- else }}
s3:
bucket: {{ required "storage.s3.bucket is required" .Values.storage.s3.bucket | quote }}
region: {{ required "storage.s3.region is required" .Values.storage.s3.region | quote }}
rootdirectory: {{ .Values.storage.s3.rootDirectory | quote }}
encrypt: true
secure: true
{{- with .Values.storage.s3.kmsKeyId }}
keyid: {{ . | quote }}
{{- end }}
{{- end }}
# Prevent clients being redirected around the internal read endpoint.
redirect:
disable: true
delete:
enabled: false
{{- end -}}
63 changes: 63 additions & 0 deletions charts/oci-registry/templates/access.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.name }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "oci.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Values.name }}
namespace: {{ .Release.Namespace }}
spec:
podSelector:
matchLabels:
{{- include "oci.selector" . | nindent 6 }}
policyTypes: ["Ingress"]
ingress:
- from:
{{- toYaml .Values.access.pullPeers | nindent 8 }}
ports:
- protocol: TCP
port: 5000
{{- if .Values.access.uploadSubjects }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Values.name }}-upload
namespace: {{ .Release.Namespace }}
rules:
- apiGroups: [""]
resources: ["services"]
resourceNames: [{{ printf "%s-upload" .Values.name | quote }}]
verbs: ["get"]
# kubectl service port-forward resolves pods with a label selector.
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/portforward"]
resourceNames: [{{ printf "%s-0" .Values.name | quote }}]
# SPDY uses POST; WebSocket upgrade uses GET (plus a CREATE check on newer APIs).
verbs: ["get", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ .Values.name }}-upload
namespace: {{ .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ .Values.name }}-upload
subjects:
{{- toYaml .Values.access.uploadSubjects | nindent 2 }}
{{- end }}
57 changes: 57 additions & 0 deletions charts/oci-registry/templates/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{{- if and (eq .Values.storage.type "pvc") (not .Values.storage.pvc.encryptionConfirmed) }}
{{- fail "PVC storage requires storage.pvc.encryptionConfirmed=true after verifying backing-storage encryption" }}
{{- end }}
{{- if empty .Values.access.pullPeers }}
{{- fail "access.pullPeers must explicitly allow Crossplane and runtime-node read traffic" }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.name }}-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "oci.labels" . | nindent 4 }}
data:
write.yml: |
version: 0.1
log:
level: warn
accesslog:
disabled: true
storage:
{{- include "oci.storage" . | nindent 6 }}
maintenance:
uploadpurging:
enabled: false
http:
# Only kubelet port-forward can reach this listener from outside the pod.
addr: 127.0.0.1:5001
relativeurls: true
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
read.yml: |
version: 0.1
log:
level: warn
accesslog:
disabled: true
storage:
{{- include "oci.storage" . | nindent 6 }}
maintenance:
readonly:
enabled: true
uploadpurging:
enabled: false
http:
addr: :5000
tls:
certificate: /certs/tls.crt
key: /certs/tls.key
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
19 changes: 19 additions & 0 deletions charts/oci-registry/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{- range $mode := list "read" "write" }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $.Values.name }}{{ if eq $mode "write" }}-upload{{ end }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "oci.labels" $ | nindent 4 }}
hops.ops.com.ai/registry-access: {{ $mode }}
spec:
type: ClusterIP
selector:
{{- include "oci.selector" $ | nindent 4 }}
ports:
- name: {{ $mode }}
port: {{ if eq $mode "read" }}5000{{ else }}5001{{ end }}
targetPort: {{ $mode }}
{{- end }}
20 changes: 20 additions & 0 deletions charts/oci-registry/templates/storage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{- if and (eq .Values.storage.type "pvc") (empty .Values.storage.pvc.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Values.name }}-pvc
namespace: {{ .Release.Namespace }}
annotations:
helm.sh/resource-policy: keep
argocd.argoproj.io/sync-options: Prune=false,Delete=false
labels:
{{- include "oci.labels" . | nindent 4 }}
spec:
accessModes: ["ReadWriteOnce"]
{{- if ne .Values.storage.pvc.storageClassName nil }}
storageClassName: {{ .Values.storage.pvc.storageClassName | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.storage.pvc.size }}
{{- end }}
Loading
Loading