Skip to content

Commit da66d00

Browse files
authored
Merge branch 'main' into fix/olm-webhookdefinitions
2 parents e170e52 + 712ed2b commit da66d00

34 files changed

Lines changed: 3038 additions & 54 deletions

.github/workflows/ci.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,42 @@ jobs:
2727
env:
2828
ENVTEST_K8S_VERSION: ${{ matrix.k8s }}.0
2929
run: make test
30+
31+
docs-build:
32+
name: Docs Build
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v6
36+
- uses: actions/setup-python@v5
37+
with:
38+
python-version: "3.12"
39+
cache: pip
40+
cache-dependency-path: docs-site/requirements.txt
41+
- name: Install docs deps
42+
run: pip install -r docs-site/requirements.txt
43+
- name: Build docs site (strict)
44+
run: mkdocs build --strict -f docs-site/mkdocs.yml
45+
- name: Upload built site as artifact
46+
if: github.event_name == 'pull_request'
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: docs-preview-${{ github.event.pull_request.number }}
50+
path: docs-site/site/
51+
retention-days: 7
52+
53+
api-docs-sync:
54+
name: API Docs Sync
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v6
58+
- uses: actions/setup-go@v6
59+
with: { go-version: "1.26" }
60+
- name: Regenerate api-reference-generated.md
61+
run: make api-docs
62+
- name: Verify no drift
63+
run: |
64+
if ! git diff --exit-code docs/api-reference-generated.md; then
65+
echo "::error::docs/api-reference-generated.md is out of sync with the API types."
66+
echo "::error::Run 'make api-docs' locally and commit the result."
67+
exit 1
68+
fi

.github/workflows/operatorhub-submit.yaml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,23 @@ jobs:
3232
VERSION="${{ steps.version.outputs.version }}"
3333
TAG="${{ steps.version.outputs.tag }}"
3434
BUNDLE_DIR="submission/operators/hermes-operator/${VERSION}"
35+
36+
# Resolve the published image to an immutable digest reference so the
37+
# submitted bundle is pinned (OLM best practice). Fall back to the
38+
# floating tag if the digest cannot be resolved (e.g. registry hiccup).
39+
# The single sed below rewrites the deployment image and the operator
40+
# relatedImage entry in the CSV to the same pinned reference.
41+
IMG="ghcr.io/paperclipinc/hermes-operator"
42+
DIGEST="$(docker buildx imagetools inspect "${IMG}:${TAG}" --format '{{ .Manifest.Digest }}' 2>/dev/null || true)"
43+
if [ -n "${DIGEST}" ]; then IMG_REF="${IMG}@${DIGEST}"; else IMG_REF="${IMG}:${TAG}"; fi
44+
echo "Pinning image to: ${IMG_REF}"
45+
3546
mkdir -p "${BUNDLE_DIR}/manifests" "${BUNDLE_DIR}/metadata"
3647
3748
# Copy and version the CSV.
3849
sed \
3950
-e "s/hermes-operator\.v[0-9]\+\.[0-9]\+\.[0-9]\+/hermes-operator.v${VERSION}/g" \
40-
-e "s|ghcr.io/paperclipinc/hermes-operator:v[0-9]\+\.[0-9]\+\.[0-9]\+|ghcr.io/paperclipinc/hermes-operator:${TAG}|g" \
51+
-e "s|ghcr.io/paperclipinc/hermes-operator:v[0-9]\+\.[0-9]\+\.[0-9]\+|${IMG_REF}|g" \
4152
-e "s/createdAt: .*/createdAt: \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"/" \
4253
-e "s/^ version: [0-9]\+\.[0-9]\+\.[0-9]\+/ version: ${VERSION}/" \
4354
bundle/manifests/hermes-operator.clusterserviceversion.yaml \
@@ -130,10 +141,21 @@ jobs:
130141
VERSION="${{ steps.version.outputs.version }}"
131142
TAG="${{ steps.version.outputs.tag }}"
132143
BUNDLE_DIR="submission/operators/hermes-operator/${VERSION}"
144+
145+
# Resolve the published image to an immutable digest reference so the
146+
# submitted bundle is pinned (OLM best practice). Fall back to the
147+
# floating tag if the digest cannot be resolved (e.g. registry hiccup).
148+
# The single sed below rewrites the deployment image and the operator
149+
# relatedImage entry in the CSV to the same pinned reference.
150+
IMG="ghcr.io/paperclipinc/hermes-operator"
151+
DIGEST="$(docker buildx imagetools inspect "${IMG}:${TAG}" --format '{{ .Manifest.Digest }}' 2>/dev/null || true)"
152+
if [ -n "${DIGEST}" ]; then IMG_REF="${IMG}@${DIGEST}"; else IMG_REF="${IMG}:${TAG}"; fi
153+
echo "Pinning image to: ${IMG_REF}"
154+
133155
mkdir -p "${BUNDLE_DIR}/manifests" "${BUNDLE_DIR}/metadata"
134156
sed \
135157
-e "s/hermes-operator\.v[0-9]\+\.[0-9]\+\.[0-9]\+/hermes-operator.v${VERSION}/g" \
136-
-e "s|ghcr.io/paperclipinc/hermes-operator:v[0-9]\+\.[0-9]\+\.[0-9]\+|ghcr.io/paperclipinc/hermes-operator:${TAG}|g" \
158+
-e "s|ghcr.io/paperclipinc/hermes-operator:v[0-9]\+\.[0-9]\+\.[0-9]\+|${IMG_REF}|g" \
137159
-e "s/createdAt: .*/createdAt: \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"/" \
138160
-e "s/^ version: [0-9]\+\.[0-9]\+\.[0-9]\+/ version: ${VERSION}/" \
139161
bundle/manifests/hermes-operator.clusterserviceversion.yaml \

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ go.work
2828
*.swp
2929
*.swo
3030
*~
31+
32+
# docs site build output and local virtualenv
33+
docs-site/site/
34+
docs-site/.venv/

Makefile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,36 @@ test: manifests generate fmt vet envtest ## Run tests.
6969
test-e2e:
7070
go test ./test/e2e/ -v -ginkgo.v
7171

72+
.PHONY: bench
73+
bench: ## Run benchmarks for resource builders.
74+
go test ./internal/resources/ -bench=. -benchmem -run=^$$ -count=1
75+
76+
##@ Docs Site
77+
78+
.PHONY: api-docs
79+
api-docs: manifests crd-ref-docs ## Regenerate docs/api-reference-generated.md from the API types.
80+
$(CRD_REF_DOCS) \
81+
--config docs-site/crd-ref-docs.yaml \
82+
--source-path api/v1 \
83+
--output-path docs/api-reference-generated.md \
84+
--renderer markdown
85+
86+
.PHONY: docs-venv
87+
docs-venv: docs-site/.venv/bin/activate ## Create the docs-site Python virtualenv.
88+
docs-site/.venv/bin/activate: docs-site/requirements.txt
89+
python3 -m venv docs-site/.venv
90+
docs-site/.venv/bin/pip install --upgrade pip
91+
docs-site/.venv/bin/pip install -r docs-site/requirements.txt
92+
touch docs-site/.venv/bin/activate
93+
94+
.PHONY: docs-serve
95+
docs-serve: docs-venv ## Serve the docs site locally (http://127.0.0.1:8000).
96+
docs-site/.venv/bin/mkdocs serve -f docs-site/mkdocs.yml
97+
98+
.PHONY: docs-build
99+
docs-build: docs-venv ## Build the docs site in strict mode (fails on warnings).
100+
docs-site/.venv/bin/mkdocs build --strict -f docs-site/mkdocs.yml
101+
72102
.PHONY: lint
73103
lint: golangci-lint ## Run golangci-lint linter
74104
$(GOLANGCI_LINT) run
@@ -210,12 +240,14 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize
210240
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
211241
ENVTEST ?= $(LOCALBIN)/setup-envtest
212242
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
243+
CRD_REF_DOCS ?= $(LOCALBIN)/crd-ref-docs
213244

214245
## Tool Versions
215246
KUSTOMIZE_VERSION ?= v5.4.2
216247
CONTROLLER_TOOLS_VERSION ?= v0.21.0
217248
ENVTEST_VERSION ?= release-0.24
218249
GOLANGCI_LINT_VERSION ?= v2.12.2
250+
CRD_REF_DOCS_VERSION ?= v0.3.0
219251

220252
.PHONY: kustomize
221253
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
@@ -237,6 +269,11 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
237269
$(GOLANGCI_LINT): $(LOCALBIN)
238270
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
239271

272+
.PHONY: crd-ref-docs
273+
crd-ref-docs: $(CRD_REF_DOCS) ## Download crd-ref-docs locally if necessary.
274+
$(CRD_REF_DOCS): $(LOCALBIN)
275+
$(call go-install-tool,$(CRD_REF_DOCS),github.com/elastic/crd-ref-docs,$(CRD_REF_DOCS_VERSION))
276+
240277
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
241278
# $1 - target path with name of binary
242279
# $2 - package url which can be installed

api/v1/hermesinstance_types.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ type HermesInstanceSpec struct {
7777
// +optional
7878
Scheduling SchedulingSpec `json:"scheduling,omitempty"`
7979

80+
// ShareProcessNamespace enables PID namespace sharing between all containers
81+
// in the pod. When true (the default), the infrastructure (pause) container
82+
// becomes PID 1 and reaps zombie processes, preventing accumulation of defunct
83+
// helper processes (git, plugins, shells) spawned under the agent entrypoint
84+
// when it does not call waitpid().
85+
//
86+
// Security note: enabling this lets every container in the pod see and signal
87+
// every other container's processes. A compromised sidecar could send signals
88+
// to the agent and vice versa. Set to false to keep per-container PID isolation;
89+
// you are then responsible for reaping zombies (e.g. by baking tini or dumb-init
90+
// into the image).
91+
// +kubebuilder:default=true
92+
// +optional
93+
ShareProcessNamespace *bool `json:"shareProcessNamespace,omitempty"`
94+
8095
// InitContainers is a user-supplied list of init containers appended after
8196
// any operator-managed init containers (e.g. runtime-init from Plan 3).
8297
// +optional
@@ -159,15 +174,23 @@ type HermesInstanceSpec struct {
159174
}
160175

161176
// ImageSpec selects an OCI image.
177+
// +kubebuilder:validation:XValidation:rule="(has(self.tag) && size(self.tag) > 0 && self.tag != 'latest') || (has(self.digest) && size(self.digest) > 0)",message="spec.image: one of tag or digest must be set and the tag must not be the floating ':latest' (pick a specific upstream release tag or pin a digest)"
162178
type ImageSpec struct {
163179
// +kubebuilder:default="ghcr.io/paperclipinc/hermes-agent"
164180
// +optional
165181
Repository string `json:"repository,omitempty"`
166182

167-
// +kubebuilder:default="latest"
183+
// Tag is the container image tag. Either tag or digest must be set; there is
184+
// no default, because pinning to a mutable tag like :latest can silently pull
185+
// a broken upstream build.
168186
// +optional
169187
Tag string `json:"tag,omitempty"`
170188

189+
// Digest overrides the tag with an image digest (e.g. sha256:abc...). When set
190+
// it takes precedence over the tag for the resolved image reference.
191+
// +optional
192+
Digest string `json:"digest,omitempty"`
193+
171194
// +kubebuilder:default=IfNotPresent
172195
// +kubebuilder:validation:Enum=Always;IfNotPresent;Never
173196
// +optional
@@ -568,6 +591,30 @@ type MetricsSpec struct {
568591
// +kubebuilder:default=false
569592
// +optional
570593
Secure *bool `json:"secure,omitempty"`
594+
595+
// GrafanaDashboard configures auto-provisioned Grafana dashboard ConfigMaps
596+
// (operator overview + per-instance). When enabled, the operator emits
597+
// ConfigMaps labeled grafana_dashboard="1" so the Grafana sidecar provisioner
598+
// picks them up automatically.
599+
// +optional
600+
GrafanaDashboard *GrafanaDashboardSpec `json:"grafanaDashboard,omitempty"`
601+
}
602+
603+
// GrafanaDashboardSpec configures auto-provisioned Grafana dashboard ConfigMaps.
604+
type GrafanaDashboardSpec struct {
605+
// Enabled enables Grafana dashboard ConfigMap creation.
606+
// +kubebuilder:default=false
607+
// +optional
608+
Enabled *bool `json:"enabled,omitempty"`
609+
610+
// Labels to add to the dashboard ConfigMaps (in addition to grafana_dashboard: "1").
611+
// +optional
612+
Labels map[string]string `json:"labels,omitempty"`
613+
614+
// Folder is the Grafana folder to place the dashboards in.
615+
// +kubebuilder:default="Hermes"
616+
// +optional
617+
Folder string `json:"folder,omitempty"`
571618
}
572619

573620
// ServiceMonitorSpec controls Prometheus-Operator ServiceMonitor emission.
@@ -1000,18 +1047,19 @@ type MigrationStatus struct {
10001047

10011048
// Condition type constants. Centralised so Plan 4-6 and docs/conditions.md stay aligned.
10021049
const (
1003-
ConditionTypeReady = "Ready"
1004-
ConditionTypeStorageReady = "StorageReady"
1005-
ConditionTypeConfigReady = "ConfigReady"
1006-
ConditionTypeSecretsReady = "SecretsReady"
1007-
ConditionTypeNetworkPolicyReady = "NetworkPolicyReady"
1008-
ConditionTypeRBACReady = "RBACReady"
1009-
ConditionTypeServiceReady = "ServiceReady"
1010-
ConditionTypePDBReady = "PDBReady"
1011-
ConditionTypeHPAReady = "HPAReady"
1012-
ConditionTypeIngressReady = "IngressReady"
1013-
ConditionTypeServiceMonitorReady = "ServiceMonitorReady"
1014-
ConditionTypePrometheusRuleReady = "PrometheusRuleReady"
1050+
ConditionTypeReady = "Ready"
1051+
ConditionTypeStorageReady = "StorageReady"
1052+
ConditionTypeConfigReady = "ConfigReady"
1053+
ConditionTypeSecretsReady = "SecretsReady"
1054+
ConditionTypeNetworkPolicyReady = "NetworkPolicyReady"
1055+
ConditionTypeRBACReady = "RBACReady"
1056+
ConditionTypeServiceReady = "ServiceReady"
1057+
ConditionTypePDBReady = "PDBReady"
1058+
ConditionTypeHPAReady = "HPAReady"
1059+
ConditionTypeIngressReady = "IngressReady"
1060+
ConditionTypeServiceMonitorReady = "ServiceMonitorReady"
1061+
ConditionTypePrometheusRuleReady = "PrometheusRuleReady"
1062+
ConditionTypeGrafanaDashboardReady = "GrafanaDashboardReady"
10151063

10161064
ConditionBackupReady = "BackupReady"
10171065
ConditionRestoreApplied = "RestoreApplied"

api/v1/zz_generated.deepcopy.go

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bundle/manifests/hermes.agent_hermesclusterdefaults.yaml

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ spec:
5555
image:
5656
description: Image defaults the instance's spec.image.
5757
properties:
58+
digest:
59+
description: |-
60+
Digest overrides the tag with an image digest (e.g. sha256:abc...). When set
61+
it takes precedence over the tag for the resolved image reference.
62+
type: string
5863
pullPolicy:
5964
default: IfNotPresent
6065
enum:
@@ -66,9 +71,18 @@ spec:
6671
default: ghcr.io/paperclipinc/hermes-agent
6772
type: string
6873
tag:
69-
default: latest
74+
description: |-
75+
Tag is the container image tag. Either tag or digest must be set; there is
76+
no default, because pinning to a mutable tag like :latest can silently pull
77+
a broken upstream build.
7078
type: string
7179
type: object
80+
x-kubernetes-validations:
81+
- message: 'spec.image: one of tag or digest must be set and the tag
82+
must not be the floating '':latest'' (pick a specific upstream
83+
release tag or pin a digest)'
84+
rule: (has(self.tag) && size(self.tag) > 0 && self.tag != 'latest')
85+
|| (has(self.digest) && size(self.digest) > 0)
7286
networking:
7387
description: Networking defaults Service kind + NetworkPolicy enablement.
7488
properties:
@@ -127,6 +141,30 @@ spec:
127141
enabled:
128142
default: true
129143
type: boolean
144+
grafanaDashboard:
145+
description: |-
146+
GrafanaDashboard configures auto-provisioned Grafana dashboard ConfigMaps
147+
(operator overview + per-instance). When enabled, the operator emits
148+
ConfigMaps labeled grafana_dashboard="1" so the Grafana sidecar provisioner
149+
picks them up automatically.
150+
properties:
151+
enabled:
152+
default: false
153+
description: Enabled enables Grafana dashboard ConfigMap
154+
creation.
155+
type: boolean
156+
folder:
157+
default: Hermes
158+
description: Folder is the Grafana folder to place the
159+
dashboards in.
160+
type: string
161+
labels:
162+
additionalProperties:
163+
type: string
164+
description: 'Labels to add to the dashboard ConfigMaps
165+
(in addition to grafana_dashboard: "1").'
166+
type: object
167+
type: object
130168
port:
131169
default: 9090
132170
description: Port for the /metrics endpoint.

0 commit comments

Comments
 (0)