Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions .github/workflows/openshift-compatibility-pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: OpenShift compatibility — PR verification

# Temporary: verifies the harness on the runner before merging okd-test.
# Delete once main has run `OpenShift compatibility` green at least once.

on:
pull_request:
branches: [main]
paths:
- '.github/workflows/openshift-compatibility.yaml'
- '.github/workflows/openshift-compatibility-pr.yaml'
- 'test/deploy/openshift_test.go'
- 'test/deploy/deploy_test.go'
- 'test/deploy/olm_manifests.yaml.tmpl'
- 'Makefile'

permissions:
contents: read

concurrency:
group: openshift-compatibility
cancel-in-progress: false

env:
KUBECONFIG: /home/runner/okd-install/auth/kubeconfig

jobs:
compat:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: [self-hosted, openshift, operator]
timeout-minutes: 45

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'

- name: Reset cluster to baseline snapshot
run: /usr/local/bin/okd-revert.sh

- name: Verify cluster health
run: |
oc get nodes
bad=$(oc get co --no-headers \
| awk '$1 != "authentication" && $1 != "monitoring" && $1 != "image-registry" \
&& ($3 != "True" || $4 != "False" || $5 != "False") { print $1 }')
if [ -n "$bad" ]; then
echo "Unhealthy ClusterOperators: $bad"
oc get co
exit 1
fi
oc wait --for=jsonpath='{.status.conditions[?(@.type=="Available")].status}=True' \
apiservice v1.project.openshift.io --timeout=5m

- name: Install OLM tooling
run: make operator-sdk opm

- name: Run OLM compatibility e2e
env:
CLICKHOUSE_VERSION: "26.3"
run: make test-compat-e2e-olm-openshift

- name: Upload test report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: openshift-compatibility-pr-report
path: "**/report/*"
if-no-files-found: warn
overwrite: true

- name: Wipe workspace
if: always()
run: rm -rf "$GITHUB_WORKSPACE/.git" "$GITHUB_WORKSPACE/_work" || true
75 changes: 75 additions & 0 deletions .github/workflows/openshift-compatibility.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: OpenShift compatibility

on:
workflow_run:
workflows: ["Publish main branch artifact"]
types: [completed]
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: openshift-compatibility
cancel-in-progress: false

env:
KUBECONFIG: /home/runner/okd-install/auth/kubeconfig

jobs:
compat:
if: |
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
runs-on: [self-hosted, openshift, operator]
timeout-minutes: 45

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'

- name: Reset cluster to baseline snapshot
run: /usr/local/bin/okd-revert.sh

- name: Verify cluster health
run: |
oc get nodes
bad=$(oc get co --no-headers \
| awk '$1 != "authentication" && $1 != "monitoring" && $1 != "image-registry" \
&& ($3 != "True" || $4 != "False" || $5 != "False") { print $1 }')
if [ -n "$bad" ]; then
echo "Unhealthy ClusterOperators: $bad"
oc get co
exit 1
fi
oc wait --for=jsonpath='{.status.conditions[?(@.type=="Available")].status}=True' \
apiservice v1.project.openshift.io --timeout=5m

- name: Install OLM tooling
run: make operator-sdk opm

- name: Run OLM compatibility e2e
env:
CLICKHOUSE_VERSION: "26.3"
run: make test-compat-e2e-olm-openshift

- name: Upload test report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: openshift-compatibility-report
path: "**/report/*"
if-no-files-found: warn
overwrite: true

- name: Wipe workspace
if: always()
run: rm -rf "$GITHUB_WORKSPACE/.git" "$GITHUB_WORKSPACE/_work" || true
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,16 @@ test-clickhouse-e2e: ## Run clickhouse e2e tests.

.PHONY: test-compat-e2e # Run compatibility smoke tests across ClickHouse versions.
test-compat-e2e: ## Run compatibility e2e tests (requires CLICKHOUSE_VERSION env var).
go test ./test/deploy/ -test.timeout 30m -v --ginkgo.v --ginkgo.label-filter=!olm --ginkgo.junit-report=report/junit-report.xml
go test ./test/deploy/ -test.timeout 30m -v --ginkgo.v --ginkgo.label-filter='!olm && !olm-openshift' --ginkgo.junit-report=report/junit-report.xml

.PHONY: test-compat-e2e-olm # Run OLM deployment smoke test.
test-compat-e2e-olm: ## Run OLM deployment e2e test on a dedicated cluster.
go test ./test/deploy/ -test.timeout 30m -v --ginkgo.v --ginkgo.label-filter=olm --ginkgo.junit-report=report/junit-report.xml

.PHONY: test-compat-e2e-olm-openshift # Run OLM deployment smoke test on OpenShift.
test-compat-e2e-olm-openshift: ## Run OLM deployment e2e against an OpenShift cluster.
go test ./test/deploy/ -test.timeout 30m -v --ginkgo.v --ginkgo.label-filter=olm-openshift --ginkgo.junit-report=report/junit-report.xml

.PHONY: test-compat-e2e-manifest # Run compatibility smoke tests (manifests deployment only).
test-compat-e2e-manifest: ## Run compatibility e2e tests using manifests deployment only (requires CLICKHOUSE_VERSION env var).
go test ./test/deploy/ -test.timeout 30m -v --ginkgo.v --ginkgo.label-filter=manifest --ginkgo.junit-report=report/junit-report.xml
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Website](https://img.shields.io/website?up_message=AVAILABLE&down_message=DOWN&url=https%3A%2F%2Fclickhouse.com&style=for-the-badge)](https://clickhouse.com)
[![Apache 2.0 License](https://img.shields.io/badge/license-Apache%202.0-blueviolet?style=for-the-badge)](https://www.apache.org/licenses/LICENSE-2.0)
[![OpenShift compatibility](https://img.shields.io/github/actions/workflow/status/ClickHouse/clickhouse-operator/openshift-compatibility.yaml?branch=main&label=OpenShift&style=for-the-badge)](https://github.com/ClickHouse/clickhouse-operator/actions/workflows/openshift-compatibility.yaml)

<picture>
<source media="(prefers-color-scheme: light)" srcset="https://clickhouse.com/docs/img/clickhouse-operator-logo-black.svg">
Expand Down
3 changes: 3 additions & 0 deletions ci/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
self-hosted-runner:
labels:
- amd-medium
- openshift
- operator
- openshift-okd
15 changes: 15 additions & 0 deletions internal/controller/clickhouse/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,16 @@ func buildVolumes(r *clickhouseReconciler, id v1.ClickHouseReplicaID) []corev1.V
})
}

// emptyDir fallback so the clickhouse data path is writable under arbitrary
// uids (e.g. OpenShift SCC). PVC path uses VolumeClaimTemplates instead.
// Skipped if the user mounts their own volume at the data path.
if r.Cluster.Spec.DataVolumeClaimSpec == nil && !controller.UserMountsAt(r.Cluster.Spec.ContainerTemplate.VolumeMounts, internal.ClickHouseDataPath) {
volumes = append(volumes, corev1.Volume{
Name: internal.PersistentVolumeName,
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
})
}

return volumes
}

Expand All @@ -722,6 +732,11 @@ func buildMounts(r *clickhouseReconciler) []corev1.VolumeMount {
SubPath: "var-log-clickhouse",
},
)
} else if !controller.UserMountsAt(r.Cluster.Spec.ContainerTemplate.VolumeMounts, internal.ClickHouseDataPath) {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: internal.PersistentVolumeName,
MountPath: internal.ClickHouseDataPath,
})
}

seenPaths := map[string]struct{}{}
Expand Down
14 changes: 9 additions & 5 deletions internal/controller/clickhouse/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ var _ = Describe("BuildVolumes", func() {
volumes := buildVolumes(&ctx, v1.ClickHouseReplicaID{})
mounts := buildMounts(&ctx)

Expect(volumes).To(HaveLen(4))
Expect(mounts).To(HaveLen(4))
Expect(volumes).To(HaveLen(5))
Expect(mounts).To(HaveLen(5))
checkVolumeMounts(volumes, mounts)
})

Expand Down Expand Up @@ -487,14 +487,18 @@ var _ = Describe("getConfigurationRevisions", func() {
})

func checkVolumeMounts(volumes []corev1.Volume, mounts []corev1.VolumeMount) {
volumeMap := map[string]struct{}{
internal.PersistentVolumeName: {},
}
// PersistentVolumeName is implicit when the cluster uses VolumeClaimTemplates
// (PVC path); the emptyDir fallback adds it explicitly to `volumes`.
volumeMap := map[string]struct{}{}
for _, volume := range volumes {
ExpectWithOffset(1, volumeMap).NotTo(HaveKey(volume.Name))
volumeMap[volume.Name] = struct{}{}
}

if _, present := volumeMap[internal.PersistentVolumeName]; !present {
volumeMap[internal.PersistentVolumeName] = struct{}{}
}

mountPaths := map[string]struct{}{}
for _, mount := range mounts {
ExpectWithOffset(1, mountPaths).NotTo(HaveKey(mount.MountPath))
Expand Down
12 changes: 12 additions & 0 deletions internal/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ type OpenSSLConfig struct {
Client OpenSSLParams `yaml:"client,omitempty"`
}

// UserMountsAt reports whether the user-provided ContainerTemplate.VolumeMounts
// claim the given mount path.
func UserMountsAt(mounts []corev1.VolumeMount, path string) bool {
for _, m := range mounts {
if m.MountPath == path {
return true
}
}

return false
}

// ProjectVolumes replaces volumes with the same mount path with a single projected volume.
func ProjectVolumes(volumes []corev1.Volume, volumeMounts []corev1.VolumeMount) ([]corev1.Volume, []corev1.VolumeMount, error) {
mountPaths := map[string][]corev1.VolumeMount{}
Expand Down
15 changes: 15 additions & 0 deletions internal/controller/keeper/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,16 @@ func buildVolumes(cr *v1.KeeperCluster, id v1.KeeperReplicaID) []corev1.Volume {
})
}

// emptyDir fallback so the keeper data path is writable under arbitrary
// uids (e.g. OpenShift SCC). PVC path uses VolumeClaimTemplates instead.
// Skipped if the user mounts their own volume at the data path.
if cr.Spec.DataVolumeClaimSpec == nil && !controller.UserMountsAt(cr.Spec.ContainerTemplate.VolumeMounts, internal.KeeperDataPath) {
volumes = append(volumes, corev1.Volume{
Name: internal.PersistentVolumeName,
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
})
}

return volumes
}

Expand Down Expand Up @@ -641,6 +651,11 @@ func buildMounts(cr *v1.KeeperCluster) []corev1.VolumeMount {
MountPath: "/var/log/clickhouse-keeper",
SubPath: "var-log-clickhouse",
})
} else if !controller.UserMountsAt(cr.Spec.ContainerTemplate.VolumeMounts, internal.KeeperDataPath) {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: internal.PersistentVolumeName,
MountPath: internal.KeeperDataPath,
})
}

if cr.Spec.Settings.TLS.Enabled {
Expand Down
Loading
Loading