goharbor

Activity

Stars and mentions · Last 30 days, Oct 6 – Nov 4

GitHub stars0
Mentions2
01234Oct 6Oct 20Nov 4
UTC days · repositories goharbor owns0 repositories

Top repositories

goharbor’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside goharbor’s account where goharbor opened or reviewed pull requests in the past 365 days.

Who noticed goharbor

Everyone who starred or forked goharbor’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

2posts
Nov 50 posts
  1. Kustomize JSON patch add: fields a Helm chart does not template A chart can only set the fields its templates write. When a Kubernetes field you need is missing from them, the value you add to values.yaml is silently ignored, unless the chart has a values schema that rejects it. This post sets nine such fields from open issues on the rendered manifests, first with a Kustomize patch over helm template, then with Kapitan's mutate generator. Most of them are RFC 6902 JSON Patch operations, and mutate's json6902 takes the same operations as Kustomize, so the two versions of each patch read almost the same. Each patch was rendered from the chart version named in its section with Helm 4.3.1, Kustomize 5.8.3, and Kapitan 0.36.3 with mutate 0.1.0. In every case the Kustomize output and the mutate output set the same value and changed nothing else in the render. The mutate output was installed on kind clusters on Kubernetes 1.37.0 with three worker nodes, and each Kustomize output's patched object passed a server-side dry run there. Charts from HTTP repositories matched the sha256 their index.yaml lists. The guide Patch a Helm chart compares the places such a patch can live. The class for each chart pins its version, like this one for Vault, and the target adds mutate as a second compile entry on the chart's output_path, as the guide shows: parameters: kapitan: dependencies: - type: helm source: https://helm.releases.hashicorp.com chart_name: vault version: 0.34.1 output_path: charts/vault compile: - input_type: helm input_paths: [charts/vault] output_path: vault helm_values_files: [values/vault.yaml] helm_params: name: vault namespace: vault Vault Helm chart: Service appProtocol {#vault-service-appprotocol} hashicorp/vault-helm#1108, opened in April 2025 and at 9 thumbs up, asks for appProtocol: https on the Vault Service. Without it, Gateway API implementations such as GKE's Gateway controller talk plain HTTP to a Vault that serves HTTPS, according to the issue and the GKE documentation it links. No maintainer has replied. Chart 0.34.1, released on 13 August 2026, writes the ports in server-service.yaml without it: ports: - name: {{ include "vault.scheme" . }} port: {{ .Values.server.service.port }} targetPort: {{ .Values.server.service.targetPort }} {{- if and (.Values.server.service.nodePort) (eq (.Values.server.service.type | toString) "NodePort") }} nodePort: {{ .Values.server.service.nodePort }} {{- end }} - name: https-internal port: 8201 targetPort: 8201 The port takes its name from the scheme, https once global.tlsDisable is false, which this values file sets: global: tlsDisable: false resources: - helm.yaml patches: - target: kind: Service name: vault patch: |- - op: test path: /spec/ports/0/name value: https - op: add path: /spec/ports/0/appProtocol value: https parameters: mutations: vault-app-protocol: match: {apiVersion: v1, kind: Service, name: vault} json6902: - {op: test, path: /spec/ports/0/name, value: https} - {op: add, path: /spec/ports/0/appProtocol, value: https} test makes both patches refuse to mark a plain HTTP port as https. With the chart's default tlsDisable: true the first port is named http, and both stopped: Error: testing value /spec/ports/0/name failed: test failed mutate: mutation vault-app-protocol: vault/templates/server-service.yaml: json6902 test of '/spec/ports/0/name' failed The Kustomize target name: vault matched the Service vault only, not vault-internal or vault-agent-injector-svc. The test applied the chart's Services to kind, without the Vault server, and the stored Service carries the field: https 8200 appProtocol=https CloudNativePG cluster chart: topologySpreadConstraints {#cloudnative-pg-topology-spread-constraints} cloudnative-pg/charts#817, opened in February 2026, asks for topologySpreadConstraints on the Cluster that the cluster chart renders. The CloudNativePG operator supports the field, but the chart's cluster.yaml only templates affinity. Pull request #818, opened the same day, adds it. It has had no review and now conflicts with the chart's main branch. In its thread, a user pointed out that the label cnpg.io/cluster alone also matches pooler and Job pods, and suggested adding cnpg.io/podRole: instance to the selector. On node-local storage that selector has a catch. CloudNativePG creates each instance's volume through an initdb or join Job, whose pod gets the instance's constraints but not the label cnpg.io/podRole, so the constraint does not count it. kind's local-path volumes are bound to the node of the first pod that mounts them. In a run with that selector, the join Job of the third instance landed on the node of the second, the volume followed it, and the third instance stayed Pending: 0/4 nodes are available: 1 node(s) didn't match pod topology spread constraints, 1 node(s) had untolerated taint(s), 2 node(s) didn't match PersistentVolume's node affinity. preemption: 0/4 nodes are available: 1 No preemption victims found for incoming pod, 3 Preemption is not helpful for scheduling. A selector that counts the Job pods too and leaves out only poolers spread all three instances. Until a chart release has the value, chart 0.9.0 takes this patch: resources: - helm.yaml patches: - target: group: postgresql.cnpg.io kind: Cluster name: pg-cluster patch: |- - op: add path: /spec/topologySpreadConstraints value: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: cnpg.io/cluster: pg-cluster matchExpressions: - key: cnpg.io/podRole operator: NotIn values: [pooler] parameters: mutations: cluster-topology-spread: match: {apiVersion: postgresql.cnpg.io/v1, kind: Cluster, name: pg-cluster} json6902: - op: add path: /spec/topologySpreadConstraints value: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: cnpg.io/cluster: pg-cluster matchExpressions: - key: cnpg.io/podRole operator: NotIn values: [pooler] With operator chart 0.29.1 on the cluster and the release named pg, the Cluster pg-cluster became Ready on a fresh cluster. Its three instance pods carry the constraint and run on three different workers: pg-cluster-1 node=set-fields-worker2 ready=true spread=kubernetes.io/hostname pg-cluster-2 node=set-fields-worker ready=true spread=kubernetes.io/hostname pg-cluster-3 node=set-fields-worker3 ready=true spread=kubernetes.io/hostname Harbor Helm chart: annotations on the core Deployment {#harbor-core-deployment-annotations} goharbor/harbor-helm#2221, opened in August 2025, asks for annotations and labels on the components' Deployments, beyond the pod annotations the chart takes. One commenter needs an annotation on harbor-core for Reloader. A collaborator suggested a Helm hook, which the reporter cannot use with Argo CD. Three pull requests for the values were closed unmerged by the stale bot. In chart 1.19.4, released on 16 October 2026, core-dpl.yaml writes no annotations into the Deployment's metadata, and core.podAnnotations, whose comment in values.yaml reads "Additional deployment annotations", goes to the pod template only: metadata: name: {{ template "harbor.core" . }} namespace: {{ .Release.Namespace | quote }} labels: {{ include "harbor.labels" . | indent 4 }} component: core app.kubernetes.io/component: core An annotation is a key in a map, which a strategic merge patch and mutate's merge add without touching other keys: resources: - helm.yaml patches: - patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: harbor-core namespace: harbor annotations: reloader.stakater.com/auto: "true" parameters: mutations: harbor-core-annotations: match: {apiVersion: apps/v1, kind: Deployment, name: harbor-core} merge: metadata: annotations: reloader.stakater.com/auto: "true" Harbor generates secrets on every render, so two renders differ in their Secrets and in the Deployments' checksum annotations. The comparison of the renders left those out. On kind, every Harbor Deployment became Available, and the stored harbor-core carries the annotation: harbor-core annotations: {"deployment.kubernetes.io/revision":"1","reloader.stakater.com/auto":"true"} ECK operator chart: ServiceMonitor labels and interval {#eck-operator-servicemonitor} elastic/cloud-on-k8s#7717, opened in April 2024, asks for labels and the interval on the operator's ServiceMonitor, because a Prometheus from kube-prometheus-stack selects ServiceMonitors by its release label. A commenter confirmed in August 2026 that 3.5.0 ignores serviceMonitor.labels and keeps a second ServiceMonitor by hand. The chart has no values schema, so the key is silently dropped. In 3.5.0, released on 4 August 2026, service-monitor.yaml renders the ServiceMonitor only with metrics in secure mode, with the chart's own labels and a fixed interval: {{- if and .Values.config.metrics.secureMode.enabled .Values.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: {{ include "eck-operator.fullname" . }} namespace: {{ ternary .Values.serviceMonitor.namespace .Release.Namespace (not (and (.Values.serviceMonitor) (empty .Values.serviceMonitor.namespace))) }} labels: {{- include "eck-operator.labels" . | nindent 4 }} spec: namespaceSelector: matchNames: - {{ .Release.Namespace }} selector: matchLabels: app.kubernetes.io/name: {{ include "eck-operator.name" . }}-metrics-service app.kubernetes.io/instance: {{ .Release.Name }} endpoints: - port: https path: /metrics scheme: https interval: 30s These values turn the metrics endpoint and secure mode on: config: metrics: port: "8080" secureMode: enabled: true resources: - helm.yaml patches: - target: group: monitoring.coreos.com kind: ServiceMonitor name: elastic-operator patch: |- - op: add path: /metadata/labels/release value: kube-prometheus-stack - op: test path: /spec/endpoints/0/interval value: 30s - op: replace path: /spec/endpoints/0/interval value: 60s parameters: mutations: eck-servicemonitor: match: {apiVersion: monitoring.coreos.com/v1, kind: ServiceMonitor, name: elastic-operator} json6902: - {op: add, path: /metadata/labels/release, value: kube-prometheus-stack} - {op: test, path: /spec/endpoints/0/interval, value: 30s} - {op: replace, path: /spec/endpoints/0/interval, value: 60s} add on /metadata/labels/release adds one key to the existing labels. The test of 30s stops the patch if a release changes the interval. On kind, with the ServiceMonitor CRD of kube-prometheus-stack 92.11.0, the operator became ready and the stored ServiceMonitor has both values: release=kube-prometheus-stack interval=60s Temporal Helm chart: Service trafficDistribution {#temporal-service-trafficdistribution} temporalio/helm-charts#897, opened in April 2026, asks for spec.trafficDistribution on the server Services, at least the frontend. Its author sets the field today with a separate server-side apply after each install. No maintainer has replied. Chart 1.7.0, released on 12 September 2026, writes type, ports and selector in server-service.yaml and has no values schema, so server.frontend.service.trafficDistribution is ignored. The chart needs a database to render, and this values file names one that the test never contacted: server: config: persistence: datastores: default: sql: pluginName: postgres12 databaseName: temporal connectAddr: db.example.internal:5432 visibility: sql: pluginName: postgres12 databaseName: temporal_visibility connectAddr: db.example.internal:5432 resources: - helm.yaml patches: - target: kind: Service name: temporal-frontend patch: |- - op: add path: /spec/trafficDistribution value: PreferSameZone parameters: mutations: temporal-traffic-distribution: match: {apiVersion: v1, kind: Service, name: temporal-frontend} json6902: - {op: add, path: /spec/trafficDistribution, value: PreferSameZone} PreferClose, the value in the issue, is the older name for PreferSameZone, and the Kubernetes documentation lists it as deprecated. The field is generally available since Kubernetes 1.33. The value PreferSameZone is enabled by default from 1.34, so a 1.33 cluster needs PreferClose. Without a database the Temporal servers cannot start, so the test applied the chart's Services only, and the API server stored the field: temporal-frontend trafficDistribution: PreferSameZone MetalLB Helm chart: the speaker without host networking {#metallb-speaker-hostnetwork} metallb/metallb#3005, opened in April 2026, asks for hostNetwork: false on the speaker DaemonSet. The reporter runs FRR on every node next to MetalLB, and FRR does not accept iBGP peering with an address in its own network namespace, so the speaker has to run in the pod network and peer with the node's FRR. Another user peers the speakers with Calico the same way. The maintainers asked for the use case. Pull request #3099 adds a speaker.hostNetwork value, and on it the maintainer fedepaol wrote "I think it makes sense to document this, mentioning that l2 won't be able to work." It is not merged. Chart 0.16.1 hard-codes the field in speaker.yaml: hostNetwork: true By default the chart runs BGP in a separate frr-k8s DaemonSet, which keeps host networking whatever the speaker does. The test therefore uses MetalLB's native mode, in which the speaker itself speaks BGP: frrk8s: enabled: false speaker: frr: enabled: false Flipping hostNetwork alone is not enough in 0.16.1. The speaker serves its health endpoint on 127.0.0.1 inside its own network namespace (speaker/main.go), and the chart's probes ask for 127.0.0.1, which the kubelet resolves in the node's network namespace. With only the field changed, no speaker became ready on kind, and every probe was refused: Readiness probe failed: Get "http://127.0.0.1:17472/readyz": dial tcp 127.0.0.1:17472: connect: connection refused Liveness probe failed: Get "http://127.0.0.1:17472/healthz": dial tcp 127.0.0.1:17472: connect: connection refused Pull request #3099 changes the field only, so it would leave the same probes in place. The patch below also removes both probes. The speaker then has no health check, and the kubelet restarts it only when the process exits: resources: - helm.yaml patches: - target: kind: DaemonSet name: metallb-speaker patch: |- - op: test path: /spec/template/spec/hostNetwork value: true - op: replace path: /spec/template/spec/hostNetwork value: false - op: test path: /spec/template/spec/containers/0/name value: speaker - op: remove path: /spec/template/spec/containers/0/livenessProbe - op: remove path: /spec/template/spec/containers/0/readinessProbe parameters: mutations: metallb-speaker-pod-network: match: {apiVersion: apps/v1, kind: DaemonSet, name: metallb-speaker} json6902: - {op: test, path: /spec/template/spec/hostNetwork, value: true} - {op: replace, path: /spec/template/spec/hostNetwork, value: false} - {op: test, path: /spec/template/spec/containers/0/name, value: speaker} - {op: remove, path: /spec/template/spec/containers/0/livenessProbe} - {op: remove, path: /spec/template/spec/containers/0/readinessProbe} On kind, a speaker ran on each of the four nodes with its own pod IP. Without a readiness probe, ready means that the container runs: metallb-speaker-6nsjd hostNetwork=false pod IP is the node IP: no ready=true restarts=0 metallb-speaker-6rr28 hostNetwork=false pod IP is the node IP: no ready=true restarts=0 metallb-speaker-8z7rr hostNetwork=false pod IP is the node IP: no ready=true restarts=0 metallb-speaker-hc622 hostNetwork=false pod IP is the node IP: no ready=true restarts=0 L2 mode answers ARP requests for the service address on the node's interface, which a speaker in the pod network does not have. With an L2 pool from the kind network and an nginx LoadBalancer Service, the test compared both settings from the host: speaker in the pod network: the LoadBalancer address from the host: not reachable speaker in the host network (plain chart): the LoadBalancer address from the host: reachable The test did not set up a BGP peer, so it shows that the speakers run in the pod network, not that they announce routes over BGP from there. Consul Helm chart: tolerations on the hook Jobs {#consul-hook-job-tolerations} hashicorp/consul-k8s#4469, opened in January 2025 and at 7 thumbs up, asks for tolerations and a nodeSelector on the Job that generates the gossip encryption key. When every node carries a taint, that Job and tls-init stay Pending, and the thread names a third Job, tls-init-cleanup. The workaround offered there is to helm pull the chart and edit its templates, which a commenter who installs from HashiCorp's repository cannot do. Pull request #4625, opened in June 2025, adds the values and has had no review. In chart 2.0.4, released on 11 September 2026, none of the three Jobs sets either field in its pod spec, and 2.1.0-rc2 does not either. All three are Helm hooks, as tls-init-job.yaml shows for tls-init: annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-weight": "1" "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation The gossip key Job runs at the same hooks, tls-init-cleanup at pre-delete. These values turn on TLS and the gossip key, and global.name gives the Jobs the names from the issue: global: name: consul tls: enabled: true gossipEncryption: autoGenerate: true The test tainted and labelled all three workers with example.com/purpose=infra and set the chart's own tolerations and nodeSelector values for the server, the connect injector and the webhook cert manager. global.acls.tolerations reaches the consul-gateway-resources Job, which a commenter also mentions, and the two gateway cleanup Jobs, even with ACLs off. Those three have no nodeSelector value. With the plain chart, both pre-install Jobs stayed Pending: consul-tls-init-fh7nh: 0/4 nodes are available: 4 node(s) had untolerated taint(s). preemption: 0/4 nodes are available: 4 Preemption is not helpful for scheduling. The three Jobs carry app: consul whatever the release is called, and component with their own name, so one Kustomize target with a set-based selector matches all three: resources: - helm.yaml patches: - target: group: batch kind: Job labelSelector: app=consul,component in (tls-init,tls-init-cleanup,gossip-encryption-autogenerate) patch: |- - op: add path: /spec/template/spec/tolerations value: - key: example.com/purpose operator: Equal value: infra effect: NoSchedule - op: add path: /spec/template/spec/nodeSelector value: example.com/purpose: infra mutate's match takes one value per key, so each Job gets a mutation of its own, and the three share the operations through an inventory key: parameters: hook_scheduling: - op: add path: /spec/template/spec/tolerations value: - {key: example.com/purpose, operator: Equal, value: infra, effect: NoSchedule} - op: add path: /spec/template/spec/nodeSelector value: example.com/purpose: infra mutations: consul-tls-init: match: {apiVersion: batch/v1, kind: Job, labels: {app: consul, component: tls-init}} json6902: ${hook_scheduling} consul-tls-init-cleanup: match: {apiVersion: batch/v1, kind: Job, labels: {app: consul, component: tls-init-cleanup}} json6902: ${hook_scheduling} consul-gossip-encryption-autogenerate: match: {apiVersion: batch/v1, kind: Job, labels: {app: consul, component: gossip-encryption-autogenerate}} json6902: ${hook_scheduling} Without a cluster to ask, helm template renders the chart's two PodDisruptionBudgets as policy/v1beta1, which Kubernetes 1.25 removed, and the first server-side dry run refused them. Both renders therefore pass --api-versions policy/v1/PodDisruptionBudget, which Kapitan takes as api_versions in helm_params. Helm hooks with the patched Jobs kubectl apply of the whole render would also start tls-init-cleanup, which deletes the Secrets consul-ca-cert, consul-ca-key and consul-server-cert. The test applied the mutate output in the order of helm install: the pre-install hooks, the templates, then the post-install Job. The server and the connect injector became ready. The pre-delete hooks, applied last as helm uninstall would run them, removed the three Secrets. Every Job completed on a tainted worker: consul-tls-init node=cac-consul-worker2 taint=example.com/purpose=infra:NoSchedule complete=True consul-gossip-encryption-autogenerate node=cac-consul-worker taint=example.com/purpose=infra:NoSchedule complete=True consul-tls-init-cleanup node=cac-consul-worker2 taint=example.com/purpose=infra:NoSchedule complete=True For helm install itself, Helm 4 passes hooks to the post-renderer together with the templates. Helm 3 never did. With Helm 3.22.1 and the same Kustomize patch as a post-renderer script, helm template printed the three Jobs without the fields. A Helm 4 post-renderer is a plugin, and this one runs the Kustomize patch from the directory it is given: name: kustomize-pr version: 0.1.0 type: postrenderer/v1 apiVersion: v1 runtime: subprocess runtimeConfig: platformCommand: - command: "${HELM_PLUGIN_DIR}/kustomize.sh" #!/usr/bin/env bash # stdin: Helm's render, hooks included. $1: the directory with kustomization.yaml. set -euo pipefail tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT cp "$1/kustomization.yaml" "$tmp/" cat > "$tmp/helm.yaml" kustomize build "$tmp" helm plugin install ./kustomize-pr helm install consul consul --repo https://helm.releases.hashicorp.com --version 2.0.4 \ -n consul -f consul-values.yaml --post-renderer kustomize-pr --post-renderer-args "$PWD" --wait The plugin's output equalled the Kustomize build. With Helm 4.3.1 on kind, the release installed, and helm uninstall ran tls-init-cleanup, each Job on a tainted worker: Successfully assigned consul/consul-gossip-encryption-autogenerate-jdm6k to cac-consul-worker Successfully assigned consul/consul-tls-init-qt7ql to cac-consul-worker3 Successfully assigned consul/consul-tls-init-cleanup-wkt87 to cac-consul-worker APISIX ingress controller chart: pod labels {#apisix-ingress-controller-pod-labels} apache/apisix-helm-chart#978, opened in June 2026 and at 11 thumbs up, asks for a podLabels value in the apisix-ingress-controller chart, so that the controller's pods can carry labels for observability, scheduling or policies. Nobody has commented. The apisix chart in the same repository got podLabels through pull request #965, merged on 30 September 2026. The controller chart takes deployment.podAnnotations, but in 1.4.0, released on 17 September 2026, deployment.yaml gives the pod template the selector labels only: labels: {{- include "apisix-ingress-controller-manager.selectorLabels" . | nindent 8 }} The chart's labelsOverride replaces these selector labels, so a label set there also lands in the Deployment's spec.selector and in the selectors of both Services. Over the installed chart, the API server refused the change: The Deployment "apisix-ingress-controller" is invalid: spec.selector: Invalid value: {"matchLabels":{"app.kubernetes.io/instance":"apisix-ingress-controller","app.kubernetes.io/name":"apisix-ingress-controller","team":"platform"}}: field is immutable add on a path below the pod template's labels sets one label there and leaves the selector alone: resources: - helm.yaml patches: - target: group: apps kind: Deployment name: apisix-ingress-controller patch: |- - op: add path: /spec/template/metadata/labels/team value: platform parameters: mutations: apisix-pod-labels: match: {apiVersion: apps/v1, kind: Deployment, name: apisix-ingress-controller} json6902: - {op: add, path: /spec/template/metadata/labels/team, value: platform} A key with a slash such as example.com/team goes into the path as example.com~1team, and both tools then set the label example.com/team. The chart generates its webhook certificate on every render, so the comparison of the renders left out that Secret and the webhook configuration's caBundle. helm template renders the chart's CRDs only with --include-crds, which Kapitan's helm input passes by default. On kind, the controller became ready, and its pod carries the label: apisix-ingress-controller-85b5bb8fb4-9m4kc labels: {"app.kubernetes.io/instance":"apisix-ingress-controller","app.kubernetes.io/name":"apisix-ingress-controller","pod-template-hash":"85b5bb8fb4","team":"platform"} Cilium Helm chart: ipFamilyPolicy on cilium-ingress {#cilium-ingress-ipfamilypolicy} cilium/cilium#27003, opened in July 2023, asks for ipFamilyPolicy and ipFamilies on the Service of Cilium's ingress controller. Without them the Service gets the cluster's primary family only, so on a dual-stack cluster the Ingresses answer on IPv4 or on IPv6, not on both. The maintainer youngnick wrote in July 2023 that LB Service customisation was planned for that cycle, and pinned the issue in December 2023. Chart 1.20.3, released on 15 October 2026, renders the Service with its ports, type, loadBalancerClass, allocateLoadBalancerNodePorts, loadBalancerIP and externalTrafficPolicy, and 1.21.0-pre.3 renders the same. The chart has this Service in shared mode only. In dedicated mode the operator creates a Service per Ingress, which a patch of the chart does not reach. The ingress controller needs Cilium's kube-proxy replacement, so the test ran Cilium as the CNI of a dual-stack kind cluster without kube-proxy: kubeProxyReplacement: true k8sServiceHost: cac-cilium-control-plane k8sServicePort: 6443 ipam: mode: kubernetes ipv6: enabled: true ingressController: enabled: true loadbalancerMode: shared resources: - helm.yaml patches: - target: kind: Service name: cilium-ingress patch: |- - op: add path: /spec/ipFamilyPolicy value: RequireDualStack - op: add path: /spec/ipFamilies value: [IPv4, IPv6] parameters: mutations: cilium-ingress-dual-stack: match: {apiVersion: v1, kind: Service, name: cilium-ingress} json6902: - {op: add, path: /spec/ipFamilyPolicy, value: RequireDualStack} - {op: add, path: /spec/ipFamilies, value: [IPv4, IPv6]} The chart generates the Hubble certificates on every render, so the comparison of the renders left out those Secrets. The plain chart gave the Service one IPv4 cluster IP. The mutate output, applied over it, changed the existing Service in place, and 60 seconds later the operator had not reverted it: ipFamilyPolicy=SingleStack ipFamilies=["IPv4"] clusterIPs=["10.96.179.251"] loadBalancer=198.51.100.0 ipFamilyPolicy=RequireDualStack ipFamilies=["IPv4","IPv6"] clusterIPs=["10.96.179.251","fd00:10:96::b12c"] loadBalancer=198.51.100.0 2001:db8:: LB-IPAM had a pool with one block of each family from the documentation ranges and gave the Service an address of each. An Ingress for an echo backend then answered on all four addresses from a node, with curl in the node's network namespace: 10.96.179.251 from node: 200, from pod: 000 fd00:10:96::b12c from node: 200, from pod: 200 198.51.100.0 from node: 200, from pod: 000 2001:db8:: from node: 200, from pod: 200 From a pod, requests over IPv4 timed out on this test host, with the plain chart as well and on an IPv4-only kind cluster too. The patch does not cause it, and the test does not cover that path. ipFamilies with both families needs a dual-stack cluster, whatever the policy. On an IPv4-only kind cluster the server-side dry run refused the patched Service: * spec.ipFamilyPolicy: Invalid value: "RequireDualStack": this cluster is not configured for dual-stack services * spec.ipFamilies[1]: Invalid value: "IPv6": not configured on this cluster With PreferDualStack it still refused ipFamilies[1]. PreferDualStack without ipFamilies passed there and got IPv4 only. Originally published at kadet-hub.org.

  2. RHELでDocker Engineを使わずにHarborを動かしてみた はじめに RHEL上にコンテナレジストリのHarborを構築したい。ただ、今回はDocker Engineではなく、Podmanを使いたい。 ということで、PodmanのDocker互換APIをDocker Composeから呼び出す構成で、Harborを動かしてみました。 HarborはDocker Composeによる構築を想定しているため、そのままでは少し引っかかるところがあります。実際、起動時にinvalid log driverというエラーが出ましたが、生成されたComposeのログ設定をsyslogからjournaldへ変更することで起動できました。 この記事では、ファイルの持ち込みから起動、OS再起動後の復帰、Trivyによる脆弱性スキャンまで、実際に確認した内容をまとめます。 まずは結果から 今回の環境では、次のところまで確認できました。 - Harbor 2.15.4を10コンテナすべてhealthyで起動できた - Docker Engineなしで、Podman 5.8.2 + Docker Compose v5.6.0を使用できた - OS再起動後にHarborが自動復帰し、journalのログも残っていた - 持ち込んだTrivy DBでイメージスキャンに成功した 一方で、通信を完全に遮断した状態でのスキャン試験や、長期運用・高負荷試験までは行っていません。あくまで今回の組み合わせでの構築・動作検証です。 :::note info Harbor公式のCompose構成はDocker Engineを前提にしています。この記事のPodman構成は、公式の標準手順ではなく実際に動かして確認した検証例として読んでください。 ::: 今回の構成 !Harbor構成図 役割をざっくり分けると、Docker Composeが起動構成を読み込み、実際のコンテナ操作はPodmanが担当します。 Docker Compose ↓ Docker互換API(/run/podman/podman.sock) Podman ↓ Harborの各コンテナ(Core / Registry / DB / Trivyなど) Harborの永続データは、OSディスクとは別に用意した30GBのXFSディスクを/dataにマウントして保存します。OS側にはRHEL、Podmanのイメージ、journalログなどが残る構成です。 検証環境 | 項目 | 今回の環境 | |---|---| | OS | RHEL 9.8 | | Kernel | 5.14.0-687.5.3.el9_8.x86_64 | | Podman / podman-docker | 5.8.2 | | Docker Compose | v5.6.0 | | Harbor | v2.15.4 | | Trivy | v0.75.0 | | ORAS | 1.3.4(Windows側) | | 仮想マシン | 6 vCPU / 約7.5GiBメモリ | | ディスク | OS 64GB / データ用30GB(XFS) | | SELinux | Enforcing | | ログ | journald(永続化あり) | RHELサーバーはインターネットに直接アクセスしない前提で作業しました。そのため、Harbor本体やTrivy DBはWindows側で取得してSCPで転送します。 1. 必要なファイルをWindowsで取得する まず、RHELへ持ち込むファイルをそろえます。 | ファイル | 用途 | |---|---| | harbor-offline-installer-v2.15.4.tgz | Harbor本体とコンテナイメージ | | docker-compose-linux-x86_64 | RHELで実行するDocker Compose | | db.tar.gz | Trivyの脆弱性DB | | javadb.tar.gz | TrivyのJava DB | 以下はWindows / PowerShellで実行します。 作業フォルダを作る $Work = Join-Path $env:TEMP 'harbor-offline' New-Item -ItemType Directory -Force $Work | Out-Null Set-Location $Work HarborとDocker Composeを取得する curl.exe -fL -o harbor-offline-installer-v2.15.4.tgz https://github.com/goharbor/harbor/releases/download/v2.15.4/harbor-offline-installer-v2.15.4.tgz curl.exe -fL -o docker-compose-linux-x86_64 https://github.com/docker/compose/releases/download/v5.6.0/docker-compose-linux-x86_64 今回はHarborのOffline Installerを使います。 ORASでTrivy DBを取得する Harborの脆弱性スキャンにはTrivyを使用します。通常、Trivyは脆弱性DBを取得・更新しますが、今回はDBも事前に持ち込むことにしました。 取得に使ったのは、OCI Artifactを扱えるORASです。 curl.exe -fL -o oras_1.3.4_windows_amd64.zip https://github.com/oras-project/oras/releases/download/v1.3.4/oras_1.3.4_windows_amd64.zip Expand-Archive oras_1.3.4_windows_amd64.zip -DestinationPath .\oras -Force .\oras\oras.exe version 今回の出力ではVersion: 1.3.4でした。 続けて、2種類のDBを取得します。 .\oras\oras.exe pull -o trivy-db ghcr.io/aquasecurity/trivy-db:2 .\oras\oras.exe pull -o trivy-java-db ghcr.io/aquasecurity/trivy-java-db:1 Copy-Item .\trivy-db\db.tar.gz . Copy-Item .\trivy-java-db\javadb.tar.gz . これで、最初の表にある4ファイルが作業フォルダにそろいます。 :::note info Vulnerability DBは脆弱性情報の照合に使うデータベースです。Java DBはJARなどのJava関連ファイルの特定に使います。今回の検証イメージはAlpineですが、Harbor側のTrivyをオフライン寄りの設定にするため、両方用意しました。 ::: 2. SCPでRHELへ転送する 転送先をRHEL側で作成します。以降、特に断りがなければRHEL側のコマンドはrootで実行しています。 mkdir -p /var/tmp/harbor-offline Windows / PowerShellから4ファイルを転送します。 scp harbor-offline-installer-v2.15.4.tgz docker-compose-linux-x86_64 db.tar.gz javadb.tar.gz "root@192.0.2.10:/var/tmp/harbor-offline/" 3. Harbor用のデータディスクを準備する HarborはイメージをPushするほどRegistryのデータが増えます。そこで今回は、OSディスクと分けて30GBの追加ディスクを/dataとして使うことにしました。 WWNで対象ディスクを特定する /dev/sdbのようなデバイス名は、環境によって変わる場合があります。誤ったディスクを操作しないように、今回はWWN(ディスクを識別するための値)を使いました。 [root@dev-1 ~]# lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS,WWN NAME SIZE TYPE FSTYPE MOUNTPOINTS WWN sda 64G disk 0x6002248049f82fbb0857ffedb5478441 ├─sda1 600M part vfat /boot/efi 0x6002248049f82fbb0857ffedb5478441 ├─sda2 1G part xfs /boot 0x6002248049f82fbb0857ffedb5478441 └─sda3 62.4G part LVM2_member ├─rhel_dev--1-root 56G lvm xfs / └─rhel_dev--1-swap 6.4G lvm swap [SWAP] sdb 30G disk 0x6002248037794f97f99303ef475df60f 今回の追加ディスクは30GBの/dev/sdbです。対応するWWNのシンボリックリンクも確認します。 [root@dev-1 ~]# ls -l /dev/disk/by-id/wwn-0x6002248037794f97f99303ef475df60f lrwxrwxrwx. 1 root root 9 Oct 16 18:03 /dev/disk/by-id/wwn-0x6002248037794f97f99303ef475df60f -> ../../sdb パーティション作成とXFSフォーマット DISK='/dev/disk/by-id/wwn-0x6002248037794f97f99303ef475df60f' PART="${DISK}-part1" parted "$DISK" --script mklabel gpt parted "$DISK" --script mkpart primary xfs 1MiB 100% partprobe "$DISK" udevadm settle mkfs.xfs -f -L harbor-data "$PART" mkdir -p /data partedでパーティションを作成し、mkfs.xfsでXFSファイルシステムにしています。partprobeとudevadm settleは、パーティション情報がOS側へ反映されるのを待つための処理です。 再起動後も/dataへマウントする 再起動しても同じ領域をマウントできるよう、UUIDを使って/etc/fstabに登録します。 UUID=$(blkid -s UUID -o value "$PART") cp -a /etc/fstab /etc/fstab.before-harbor printf 'UUID=%s /data xfs defaults 0 0\n' "$UUID" >> /etc/fstab systemctl daemon-reload mount /data マウントされたか確認します。 [root@dev-1 ~]# findmnt /data TARGET SOURCE FSTYPE OPTIONS /data /dev/sdb1 xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota [root@dev-1 ~]# df -h /data Filesystem Size Used Avail Use% Mounted on /dev/sdb1 30G 247M 30G 1% /data ここまでで、30GBのXFS領域を/dataに用意できました。 :::note info 今回は/etc/fstabにnofailを付けていません。さらに後述のharbor.serviceでも、/dataがマウントされていなければHarborを起動しないようにします。データディスクが見つからないまま、空の/dataへ書き込まれることを避けるためです。 ::: 4. PodmanとDocker Composeを準備する 今回の構成では、Composeファイルを読むのがDocker Compose、コンテナを実行するのがPodmanです。 RHELのDNFリポジトリからPodmanを導入します。 [root@dev-1 ~]# dnf install -y podman-5.8.2 podman-docker-5.8.2 (依存パッケージの表示は省略) Complete! [root@dev-1 ~]# podman --version podman version 5.8.2 Docker ComposeがPodmanを操作できるよう、Docker互換APIのソケットを有効にします。 [root@dev-1 ~]# systemctl enable --now podman.socket [root@dev-1 ~]# ls -l /var/run/docker.sock /run/podman/podman.sock srw-rw----. 1 root root 0 ... /run/podman/podman.sock lrwxrwxrwx. 1 root root 23 ... /var/run/docker.sock -> /run/podman/podman.sock 次に、Windowsから持ち込んだDocker Composeを配置します。 [root@dev-1 ~]# install -m 0755 /var/tmp/harbor-offline/docker-compose-linux-x86_64 /usr/local/bin/docker-compose [root@dev-1 ~]# docker-compose version Docker Compose version v5.6.0 これで、ComposeからPodmanを操作する準備が整いました。 5. Harborを展開・設定する Harbor本体とコンテナイメージを展開する tar -xzf /var/tmp/harbor-offline/harbor-offline-installer-v2.15.4.tgz -C /opt podman load -i /opt/harbor/harbor.v2.15.4.tar.gz 検証ではどちらも正常終了し、Harborのイメージをローカルに登録できました。 HTTPS用の自己署名証明書を作る HarborへのアクセスはHTTPSにします。今回は検証環境なので、自己署名証明書を使用しました。 IPアドレスで接続するため、証明書のSAN(Subject Alternative Name)にもIPアドレスを含めます。 HARBOR_IP=192.0.2.10 mkdir -p /data/cert openssl req -x509 -newkey rsa:3072 -sha256 -days 365 -nodes \ -subj "/CN=$HARBOR_IP" \ -addext "subjectAltName=IP:$HARBOR_IP" \ -keyout /data/cert/harbor.key \ -out /data/cert/harbor.crt chmod 600 /data/cert/harbor.key 作成したファイルは、証明書がharbor.crt、秘密鍵がharbor.keyです。-days 365で有効期間を365日に設定しています。 証明書の内容も確認しておきます。 openssl x509 -in /data/cert/harbor.crt -noout -subject -dates -ext subjectAltName Podmanに証明書を登録する 自己署名証明書は、そのままではPodmanから信頼されません。今回はレジストリごとの証明書ディレクトリへ配置しました。 mkdir -p "/etc/containers/certs.d/$HARBOR_IP" install -m 0644 /data/cert/harbor.crt "/etc/containers/certs.d/$HARBOR_IP/ca.crt" :::note warn この設定で証明書を信頼するのは、このRHEL上のPodmanです。別のPCからHarborへ接続する場合は、そのPCのブラウザやコンテナクライアント側でも証明書の信頼設定が必要になる場合があります。本番環境では、信頼されたCAが発行した証明書の利用を検討してください。 ::: firewalldでHTTPSを許可する RHEL側でTCP 443番ポートへの通信を許可します。 [root@dev-1 ~]# firewall-cmd --permanent --add-service=https success [root@dev-1 ~]# firewall-cmd --reload success harbor.ymlを設定する Harborのテンプレートをコピーし、設定ファイルを編集します。 cd /opt/harbor cp -a harbor.yml.tmpl harbor.yml vi harbor.yml 主な変更点は次のとおりです。 hostname: 192.0.2.10 https: port: 443 certificate: /data/cert/harbor.crt private_key: /data/cert/harbor.key harbor_admin_password: <Harbor管理者用のパスワード> database: password: <PostgreSQL用のパスワード> data_volume: /data trivy: skip_update: true skip_java_db_update: true offline_scan: true :::note info 上記は変更する項目だけの抜粋です。実際にはharbor.yml.tmplの他の項目も残してください。また、管理者用とPostgreSQL用には別々のパスワードを設定します。 ::: 今回はTrivy DBを手動で持ち込むので、skip_updateとskip_java_db_updateをtrueにしました。offline_scanは、スキャン中のJava関連の外部問い合わせを抑えるための設定です。 Trivy DBを配置する まず、Trivy Adapterの実行ユーザーを確認しました。 [root@dev-1 harbor]# podman run --rm --pull=never --entrypoint /usr/bin/id localhost/goharbor/trivy-adapter-photon:v2.15.4 uid=10000(scanner) gid=10000(scanner) groups=10000(scanner) UID/GIDがどちらも10000だったので、その値で所有者を設定します。 mkdir -p /data/trivy-adapter/trivy/db /data/trivy-adapter/trivy/java-db /data/trivy-adapter/reports tar -xzf /var/tmp/harbor-offline/db.tar.gz -C /data/trivy-adapter/trivy/db tar -xzf /var/tmp/harbor-offline/javadb.tar.gz -C /data/trivy-adapter/trivy/java-db chown -R 10000:10000 /data/trivy-adapter 展開後、trivy.db、trivy-java.dbと、それぞれのmetadata.jsonが配置されました。 prepareでComposeを生成する Harborのprepareは、harbor.ymlをもとに各サービスの設定とdocker-compose.ymlを生成します。 今回のPodman環境では、bind mount元のディレクトリを事前に作成してから実行しました。 cd /opt/harbor mkdir -p /opt/harbor/common/config ./prepare --with-trivy 出力には、次のファイルの生成が含まれていました。 Generated configuration file: /config/core/env Generated configuration file: /config/registry/config.yml Generated configuration file: /config/db/env Generated configuration file: /config/trivy-adapter/env Generated configuration file: /compose_location/docker-compose.yml 6. Composeのログ設定をsyslogからjournaldへ変更する Harborが生成したComposeには、次のようなログ設定があります。 logging: driver: "syslog" options: syslog-address: "tcp://localhost:1514" tag: "registry" これを変更せずPodmanのDocker互換APIで起動すると、invalid log driverで失敗しました。 Podmanが対応するログドライバと、HarborのComposeが指定するログドライバに違いがあるためです。そこで、今回はjournaldに変更します。 生成ファイルをバックアップして修正する cd /opt/harbor cp -a docker-compose.yml docker-compose.generated.yml grep -c 'driver: "syslog"' docker-compose.generated.yml # 9 sed -i 's/driver: "syslog"/driver: "journald"/g' docker-compose.yml sed -i '/^[[:space:]]*syslog-address:/d' docker-compose.yml grep -c 'driver: "journald"' docker-compose.yml # 9 syslog-addressも不要になるので削除します。tagはそのまま残しました。 修正後のイメージは次のとおりです。 logging: driver: "journald" options: tag: "registry" Composeとして読み込めるか確認します。 export DOCKER_HOST=unix:///run/podman/podman.sock docker-compose config -q config -qがエラーなく終了すれば、Composeファイルの構文チェックは通っています。 ログの保存先が変わることに注意 :::note warn ここで変更しているのは、単にエラーを回避するための文字列ではありません。コンテナの標準出力・標準エラーの保存経路も変わります。 Harbor標準のsyslog経由の収集ではなく、今回の構成ではホストのjournaldに記録されます。harbor-logコンテナが起動していても、標準構成と同じログ収集経路を使っているわけではありません。 ::: 実際、harbor-logには明示的なログドライバ設定がありませんでしたが、今回のPodman環境では既定のjournaldが使われました。 また、prepareを再実行するとComposeが再生成される点にも注意が必要です。Harborの設定変更やアップグレード後には、このjournaldへの修正を再適用して確認してください。 journalを永続化する せっかくログが出ても、再起動で消えてしまうと困ります。今回はjournalを永続保存するように設定しました。 mkdir -p /etc/systemd/journald.conf.d cat > /etc/systemd/journald.conf.d/60-harbor-persistent.conf <<'EOF' [Journal] Storage=persistent SystemMaxUse=512M EOF systemctl restart systemd-journald journalctl --flush Storage=persistentで再起動後もログを残し、SystemMaxUse=512Mで使用容量を制限します。 ここでの512MBはHarborだけでなく、ホストのjournal全体に対する上限です。今回は大量ログ発生時のローテーション試験までは行っていません。 7. Harborを起動してみる いよいよ起動です。今回は持ち込んだイメージだけを使うため、--pull neverを指定しました。 cd /opt/harbor export DOCKER_HOST=unix:///run/podman/podman.sock docker-compose up -d --pull never 起動直後、JobServiceがCoreの準備完了より先に接続しようとして、一時的に終了する場面がありました。 Failed on load rest config ... Get "http://core:8080/api/v2.0/internalconfig": connect: connection refused panic: failed to load configuration, error: failed to load rest config 少し焦ったログですが、今回はrestart: alwaysによる再起動で復旧しました。手動でComposeを修正したり、コンテナを再作成したりする必要はありませんでした。 全コンテナの状態を確認する [root@dev-1 ~]# podman inspect --format '{{.Name}} {{.State.Status}} {{.State.Health.Status}} log={{.HostConfig.LogConfig.Type}}' $(podman ps -aq) harbor-log running healthy log=journald harbor-portal running healthy log=journald registry running healthy log=journald redis running healthy log=journald registryctl running healthy log=journald harbor-db running healthy log=journald trivy-adapter running healthy log=journald harbor-core running healthy log=journald nginx running healthy log=journald harbor-jobservice running healthy log=journald 10コンテナすべてrunningかつhealthyになりました。 HarborのHealth APIも確認します。$HARBOR_IPが未設定の場合は、あらためてIPを設定してください。 curl --silent --show-error --fail --cacert /data/cert/harbor.crt "https://192.0.2.10/api/v2.0/health" レスポンスは次のとおりでした(整形しています)。 { "components": [ {"name": "core", "status": "healthy"}, {"name": "database", "status": "healthy"}, {"name": "jobservice", "status": "healthy"}, {"name": "portal", "status": "healthy"}, {"name": "redis", "status": "healthy"}, {"name": "registry", "status": "healthy"}, {"name": "registryctl", "status": "healthy"}, {"name": "trivy", "status": "healthy"} ], "status": "healthy" } コンテナの状態だけでなく、Harbor自身も正常と判断していることが確認できました。 journalctlでもログを確認する [root@dev-1 harbor]# journalctl -t core --no-pager -n 30 Oct 16 18:22:20 dev-1 core[17830]: 2026-10-16T09:22:20Z [INFO] [/core/main.go:279]: Version: v2.15.4, Git commit: d8841742 podman logsからも確認できます。 podman logs harbor-core journalctl CONTAINER_NAME=harbor-core --no-pager -n 30 今回の検証では、Coreで29件、Trivy Adapterで5件について、両方の方法で同じメッセージを取得できました。 8. systemdでOS起動時に自動復帰させる 手動で起動できても、サーバー再起動のたびに操作が必要では使いにくいですよね。 そこでHarbor全体をsystemdから起動するようにしました。ポイントは、Podmanソケットと/dataのマウントを前提条件にすることです。 harbor.serviceを作成する /etc/systemd/system/harbor.serviceに以下を保存します。 [Unit] Description=Harbor Container Registry (Docker Compose on Podman) Wants=network-online.target After=network-online.target podman.socket Requires=podman.socket RequiresMountsFor=/data [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/opt/harbor Environment=DOCKER_HOST=unix:///run/podman/podman.sock ExecStartPre=/usr/bin/mountpoint -q /data ExecStart=/usr/local/bin/docker-compose up -d --pull never ExecStop=/usr/local/bin/docker-compose stop TimeoutStartSec=0 TimeoutStopSec=120 [Install] WantedBy=multi-user.target RequiresMountsFor=/dataで必要なマウントを要求し、さらにmountpoint -q /dataで実際にマウントされていることを確認しています。 systemctl daemon-reload systemctl enable --now harbor.service 実際にOSを再起動してみる systemctl reboot 再ログイン後、まずサービスを確認しました。 [root@dev-1 ~]# systemctl status harbor.service --no-pager ● harbor.service - Harbor Container Registry (Docker Compose on Podman) Loaded: loaded (/etc/systemd/system/harbor.service; enabled; preset: disabled) Active: active (exited) since Fri 2026-10-16 18:23:27 JST Process: 919 ExecStartPre=/usr/bin/mountpoint -q /data (code=exited, status=0/SUCCESS) Process: 932 ExecStart=/usr/local/bin/docker-compose up -d --pull never (code=exited, status=0/SUCCESS) active (exited)と表示されていますが、今回はType=oneshot + RemainAfterExit=yesなので想定どおりです。 その後、手動でdocker-compose upを実行することなく、10コンテナすべてとHealth APIが再びhealthyになったことを確認しました。 journalも再起動前のログが残っていました。 [root@dev-1 ~]# journalctl --list-boots IDX BOOT ID FIRST ENTRY LAST ENTRY -1 617ae65b8de8400189a9610560885380 Fri 2026-10-16 18:17:26 JST Fri 2026-10-16 18:23:14 JST 0 15981601a8384a6fac7280fdf6cb7c82 Fri 2026-10-16 18:23:18 JST Fri 2026-10-16 18:24:00 JST これで、Harborの自動復帰とログの永続化まで確認できました。 9. Trivyでイメージをスキャンする 最後に、手動で搬入したDBを使ってスキャンできるか試します。 オフライン向け設定が反映されているか確認 [root@dev-1 harbor]# grep -E 'SKIP_UPDATE|SKIP_JAVA_DB_UPDATE|OFFLINE_SCAN' /opt/harbor/common/config/trivy-adapter/env SCANNER_TRIVY_SKIP_UPDATE=True SCANNER_TRIVY_SKIP_JAVA_DB_UPDATE=True SCANNER_TRIVY_OFFLINE_SCAN=True 3つとも有効になっています。 検証用のAlpineイメージを用意する 今回はAlpine 3.18.0を使いました。実際の検証ではWindows側の補助スクリプトでOCIアーカイブを作成し、RHELへSCPで転送しています。 同じ形式のファイルを用意する方法として、インターネットに接続でき、Podmanを利用できる別マシンなら次の方法もあります(こちらは再現用の例で、今回実測したWindowsの補助スクリプトそのものではありません)。 podman pull --platform linux/amd64 docker.io/library/alpine:3.18.0 podman save --format oci-archive -o alpine-3.18.0-amd64.oci.tar docker.io/library/alpine:3.18.0 作成したalpine-3.18.0-amd64.oci.tarを、先ほどと同様にRHELの/var/tmp/harbor-offline/へ転送します。 RHEL側でロードします。 [root@dev-1 ~]# podman load -i /var/tmp/harbor-offline/alpine-3.18.0-amd64.oci.tar Loaded image: docker.io/library/alpine:3.18.0 :::note info ここでのイメージ取得・アーカイブ作成は再現用の一例です。元の検証で使用したWindowsの補助スクリプトの詳細は省略しています。アーカイブの作成方法によってはロード後のイメージ名を確認し、podman tagの参照元を調整してください。 ::: HarborへPushする まずHarborにverificationプロジェクトを作成します。今回の検証ではHarbor APIから作成しましたが、Web UIから同名のプロジェクトを用意しても構いません。 次は読者向けの簡略化した実行例です。実測時はパスワードを標準入力から渡し、専用の認証ファイルを指定して実施しました。 podman login -u admin 192.0.2.10 podman tag docker.io/library/alpine:3.18.0 "192.0.2.10/verification/alpine:3.18.0" podman push "$HARBOR_IP/verification/alpine:3.18.0" 実測ではLogin Succeeded!が表示され、Pushも終了コード0で成功しました。 スキャンを実行して結果を確認する 今回はHarbor APIからスキャンを要求しました。 POST /api/v2.0/projects/verification/repositories/alpine/artifacts/3.18.0/scan HTTP 202 GET /api/v2.0/projects/verification/repositories/alpine/artifacts/3.18.0?with_scan_overview=true HTTP 200 scan_status: Success scanner: Trivy v0.75.0 取得したレポートの集計結果は以下のとおりです。 | 深刻度 | 件数 | |---|---:| | Critical | 3 | | High | 6 | | Medium | 26 | | Low | 14 | | 合計 | 49 | 49件はパッケージごとの検出件数で、異なる脆弱性IDは22種類でした。 持ち込んだDBでスキャンが成功したことは確認できました。ただし、検出件数はDBの更新時点や対象イメージのdigestによって変わります。 10. ディスク使用量を確認する 最後に、OSディスクとHarborのデータ領域がどれくらい使われたか確認しました。 df -h / /data 実測時の使用量は次のとおりです(デバイス名ではなくマウント先で整理しています)。 | マウント先 | 容量 | 使用量 | 使用率 | |---|---:|---:|---:| | / | 56GB | 7.0GB | 13% | | /data | 30GB | 3.3GB | 11% | ディレクトリ別にも見てみました。 [root@dev-1 ~]# du -sh /data/database /data/registry /data/trivy-adapter /var/lib/containers/storage /var/log/journal 67M /data/database 3.4M /data/registry 3.0G /data/trivy-adapter 3.7G /var/lib/containers/storage 8.1M /var/log/journal 今回は小規模な検証用Registryなので、/data/registryは数MBです。一方、/dataの大部分はTrivy関連のデータが占めていました。 また、data_volume: /dataを設定しても、PodmanのイメージはOS側の/var/lib/containers/storageに保存されます。今回のjournaldログもOS側です。 つまり、Harborのデータディスクを分けても、OSディスクの容量監視は必要ということですね。 まとめ 今回は、RHEL 9.8上でDocker Engineを使わず、Podman + Docker Composeを使ってHarborを構築しました。 最初は「Docker ComposeをPodmanへ向ければ、そのまま動くのでは?」と思っていたのですが、やはり細かい差分はありますね。特にログの扱いは、起動させるだけでなく、あとから調査するときにも影響する部分だと感じました。 本番運用へ持っていくなら、少なくとも証明書の管理、Trivy DBの定期更新、ログの保全、prepare再実行時のCompose修正手順は別途整備したいところです。 同じようにRHEL + PodmanでHarborを動かしたい方の参考になれば幸いです。 参考 - Harbor公式ドキュメント - Harbor v2.15.4 リリース - Harborのシステム要件 - Harborの設定変更とprepare - Podman公式ドキュメント - Podman Docker互換API - Docker Compose - TrivyのAir-Gapped環境に関する説明 - ORAS

Achievements

Awards earned by the repositories goharbor owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.