Skip to content
Merged
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
23 changes: 20 additions & 3 deletions .github/workflows/coherence-matrix.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021, 2025, Oracle Corporation and/or its affiliates. All rights reserved.
# Copyright 2021, 2026, Oracle Corporation and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at
# http://oss.oracle.com/licenses/upl.

Expand Down Expand Up @@ -49,8 +49,8 @@ jobs:
- "12.2.1-4-SNAPSHOT"
include:
- matrixName: "15.1.1-0-SNAPSHOT"
coherenceVersion: "15.1.1-0-3-SNAPSHOT"
coherenceImage: "iad.ocir.io/odx-stateservice/test/coherence:15.1.1-0-3-SNAPSHOT-java17"
coherenceVersion: "15.1.1-0-5-SNAPSHOT"
coherenceImage: "iad.ocir.io/odx-stateservice/test/coherence:15.1.1-0-5-SNAPSHOT-java17"
javaVersion: 17
coherenceIsJava8: false
baseImage: "gcr.io/distroless/java17-debian12"
Expand Down Expand Up @@ -229,6 +229,23 @@ jobs:
docker pull gcr.io/distroless/java21-debian12
docker pull "${COHERENCE_IMAGE}"

- name: Health mutator protocol regressions
shell: bash
run: |
make generate
go test ./pkg/probe ./controllers/finalizer ./controllers/statefulset
./mvnw -B -f java -pl coherence-operator -am test -Dtest=SecretLoginModuleTest,HealthSSLProviderConfigTest -Dsurefire.failIfNoSpecifiedTests=false
if [[ "${{ matrix.matrixName }}" == "15.1.1-0-SNAPSHOT" ]]; then
test/health-mutator/extract-coherence-jar.sh \
"${{ matrix.coherenceImage }}" \
"${{ matrix.coherenceVersion }}" \
/tmp/health-mutator/coherence.jar
./mvnw -B -f java -pl coherence-operator -am test \
-Dtest=HealthSecuritySupportTest \
-Dhealth.mutator.coherence.jar=/tmp/health-mutator/coherence.jar \
-Dsurefire.failIfNoSpecifiedTests=false
fi

- name: Coherence Certification Tests
shell: bash
run: |
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/compatibility-tests.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2019, 2025, Oracle Corporation and/or its affiliates. All rights reserved.
# Copyright 2019, 2026, Oracle Corporation and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at
# http://oss.oracle.com/licenses/upl.

Expand Down Expand Up @@ -164,6 +164,12 @@ jobs:
sudo docker builder prune -a
df -h

- name: Health mutator protocol regressions
shell: bash
run: |
go test ./pkg/probe ./controllers/finalizer ./controllers/statefulset
./mvnw -B -f java -pl coherence-operator -am test -Dtest=SecretLoginModuleTest,HealthSSLProviderConfigTest -Dsurefire.failIfNoSpecifiedTests=false

- name: Compatibility Tests
shell: bash
run: |
Expand Down
167 changes: 167 additions & 0 deletions .github/workflows/health-mutator-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright (c) 2026, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at
# http://oss.oracle.com/licenses/upl.
name: Health mutator tests
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
client-and-admission:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: .go-version
- name: Generate API and schema
run: make generate manifests
- name: Client and controller tests
run: |
go test ./api/... ./controllers/... ./pkg/probe
go test ./pkg/runner -run '^(TestManagedHealth|TestHealthSecurityCheck)'
- name: Install isolated API server
run: |
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@32e5e9e948a572779280969aaaf7db92f76d8252
echo "KUBEBUILDER_ASSETS=$(setup-envtest use -p path 1.30.x!)" >> "$GITHUB_ENV"
- name: Admission tests
run: HEALTH_MUTATOR_RUNNER_SUITE=true go test -v ./test/health-mutator
java-support:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java: ['8', '17', '25']
steps:
- uses: actions/checkout@v4
- name: Install build JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
# Compile Java 8 bytecode with JDK 17 because the Spring build dependency requires JDK 17.
# Surefire runs these focused tests on the selected Java runtime. They do not exercise the
# Coherence 21.12.5 code path that requires the legacy JDK 17 toolchain on Java 24 and later.
- name: Test packaged validator and TLS provider mapping
run: |
test_java="${JAVA_HOME}/bin/java"
JAVA_HOME="${JAVA_HOME_17_X64}" ./mvnw -B -f java -pl coherence-operator -am test \
-P=-legacy-coherence-toolchain \
-Djvm="${test_java}" \
-Dtest=SecretLoginModuleTest,HealthSSLProviderConfigTest \
-Dsurefire.failIfNoSpecifiedTests=false
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: health-mutator-java-support-${{ matrix.java }}
path: java/coherence-operator/target/surefire-reports/
runtime-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
COHERENCE_VERSION_LTS: 15.1.1-0-5
KIND_CLUSTER: health-mutator
OPERATOR_IMAGE_REGISTRY: health-mutator.local
OPERATOR_IMAGE_TAG: ${{ github.sha }}
TEST_APPLICATION_IMAGE: health-mutator.local/operator-test:${{ github.sha }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: .go-version
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- name: Install runtime test tools
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends --yes gettext-base
- name: Build candidate and test application images
run: make build-operator build-basic-test-image
- name: Create isolated Kind cluster
run: make kind-single-worker
- name: Load digest-pinned images
id: images
shell: bash
run: |
operator_image="${OPERATOR_IMAGE_REGISTRY}/coherence-operator:${OPERATOR_IMAGE_TAG}"

build/tools/bin/kind load docker-image --name "${KIND_CLUSTER}" "${operator_image}"
build/tools/bin/kind load docker-image --name "${KIND_CLUSTER}" "${TEST_APPLICATION_IMAGE}"

first_node=$(build/tools/bin/kind get nodes --name "${KIND_CLUSTER}" | head -n 1)
operator_manifest_digest=$(docker exec "${first_node}" ctr --namespace k8s.io images list | \
awk -v ref="${operator_image}" '$1 == ref {print $3}')
fixture_manifest_digest=$(docker exec "${first_node}" ctr --namespace k8s.io images list | \
awk -v ref="${TEST_APPLICATION_IMAGE}" '$1 == ref {print $3}')
operator_digest="${operator_image%:*}@${operator_manifest_digest}"
fixture_digest="${TEST_APPLICATION_IMAGE%:*}@${fixture_manifest_digest}"
if [[ "${operator_digest}" != *@sha256:* || "${fixture_digest}" != *@sha256:* ]]; then
echo "The locally built images do not have immutable manifest digests" >&2
exit 1
fi

while read -r node; do
docker exec "${node}" ctr --namespace k8s.io images tag --force \
"${operator_image}" "${operator_digest}"
docker exec "${node}" ctr --namespace k8s.io images tag --force \
"${TEST_APPLICATION_IMAGE}" "${fixture_digest}"
done < <(build/tools/bin/kind get nodes --name "${KIND_CLUSTER}")

echo "operator_digest=${operator_digest}" >> "${GITHUB_OUTPUT}"
echo "fixture_digest=${fixture_digest}" >> "${GITHUB_OUTPUT}"
- name: Deploy candidate Operator
shell: bash
run: |
make prepare-deploy
(
cd build/_output/config/manager
"${GITHUB_WORKSPACE}/build/tools/bin/kustomize" edit set image \
"controller=${{ steps.images.outputs.operator_digest }}"
)
kubectl create namespace operator-test --dry-run=client -o yaml | kubectl apply -f -
build/tools/bin/kustomize build build/_output/config/default | kubectl apply -f -
kubectl -n operator-test scale \
deployment/coherence-operator-controller-manager --replicas=1
kubectl -n operator-test rollout status \
deployment/coherence-operator-controller-manager --timeout=10m
deployed_image=$(kubectl -n operator-test get \
deployment/coherence-operator-controller-manager \
-o jsonpath='{.spec.template.spec.containers[0].image}')
if [[ "${deployed_image}" != "${{ steps.images.outputs.operator_digest }}" ]]; then
echo "The deployed Operator image does not match the locally built digest" >&2
exit 1
fi
- name: Run health mutator runtime acceptance
shell: bash
run: |
context="kind-${KIND_CLUSTER}"
namespace=health-mutator-e2e
material="${RUNNER_TEMP}/health-mutator-e2e"
bash test/health-mutator/apply.sh \
"${context}" "${namespace}" "${{ steps.images.outputs.fixture_digest }}" "${material}"
bash test/health-mutator/verify.sh "${context}" "${namespace}" "${material}"
- name: Collect failure diagnostics
if: failure()
shell: bash
run: |
context="kind-${KIND_CLUSTER}"
kubectl --context "${context}" get nodes -o wide || true
kubectl --context "${context}" get pods,statefulsets,pvc -A -o wide || true
kubectl --context "${context}" -n health-mutator-e2e \
get coherence.coherence.oracle.com health-secure -o yaml || true
kubectl --context "${context}" -n operator-test logs \
-l control-plane=coherence --all-containers --prefix --tail=1000 || true
kubectl --context "${context}" get events -A \
--sort-by=.metadata.creationTimestamp || true
- name: Delete isolated Kind cluster
if: always()
run: make kind-stop
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ bundle.Dockerfile
node_modules
.coh-history
.cohql-history
runner
venv

.oca/
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -823,10 +823,12 @@ config/crd/bases/coherence.oracle.com_coherence.yaml: $(TOOLS_BIN)/kustomize $(A
$(CONTROLLER_GEN) "crd:crdVersions={v1}" \
rbac:roleName=manager-role paths="{./api/...,./controllers/...}" \
output:crd:dir=config/crd/bases
go run ./utils/crd-fixup/ config/crd/bases/*.yaml
cp -R config/crd/ config/crd-small
$(CONTROLLER_GEN) "crd:crdVersions={v1},maxDescLen=0" \
rbac:roleName=manager-role paths="{./api/...,./controllers/...}" \
output:crd:dir=config/crd-small/bases
go run ./utils/crd-fixup/ config/crd-small/bases/*.yaml
$(YQ) eval -i '.metadata.labels["app.kubernetes.io/version"] = "$(VERSION)"' config/crd/bases/coherence.oracle.com_coherence.yaml
$(YQ) eval -i '.metadata.labels["app.kubernetes.io/version"] = "$(VERSION)"' config/crd/bases/coherence.oracle.com_coherencejob.yaml
$(YQ) eval -i '.metadata.labels["app.kubernetes.io/version"] = "$(VERSION)"' config/crd-small/bases/coherence.oracle.com_coherence.yaml
Expand Down Expand Up @@ -872,6 +874,7 @@ docs/about/04_coherence_spec.adoc: $(API_GO_FILES) utils/docgen/main.go
api/v1/coherence_types.go \
api/v1/coherenceresource_types.go \
api/v1/coherencejobresource_types.go \
api/v1/health_mutator.go \
> docs/about/04_coherence_spec.adoc

# ----------------------------------------------------------------------------------------------------------------------
Expand Down
82 changes: 82 additions & 0 deletions api/v1/coherence_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2319,6 +2319,7 @@ func (in *ServiceSpec) createServiceSpec() corev1.ServiceSpec {

// ScalingSpec is the configuration to control safe scaling.
// +k8s:openapi-gen=true
// +kubebuilder:validation:XValidation:rule="!has(self.probe) || !has(self.probe.http) || !has(self.probe.http.method) || self.probe.http.method == 'GET'",message="HA requires a read-only GET action"
type ScalingSpec struct {
// ScalingPolicy describes how the replicas of the deployment will be scaled.
// The default if not specified is based upon the value of the StorageEnabled field.
Expand All @@ -2343,15 +2344,96 @@ type ScalingSpec struct {
// StatusHA checking is primarily used during scaling of a deployment, a deployment must be in a safe Phase HA
// state before scaling takes place. If StatusHA handler is disabled for a deployment (by specifically setting
// Enabled to false then no check will take place and a deployment will be assumed to be safe).
// Empty and gRPC-only handlers remain successful no-ops for backward compatibility. The Operator emits a Warning
// event because these handlers do not execute a request or command and do not prove StatusHA or service suspension.
// +k8s:openapi-gen=true
// +kubebuilder:validation:XValidation:rule="!has(self.http) || (!has(self.exec) && !has(self.httpGet) && !has(self.tcpSocket) && !has(self.grpc))",message="http cannot be combined with a legacy probe action"
type Probe struct {
// HTTP is a method-aware request executed by the Operator.
// +optional
HTTP *HTTPAction `json:"http,omitempty"`
corev1.ProbeHandler `json:",inline"`
// Number of seconds after which the handler times out (only applies to http and tcp handlers).
// Defaults to 1 second. Minimum value is 1.
// +optional
TimeoutSeconds *int `json:"timeoutSeconds,omitempty"`
}

// HTTPAction is an Operator HTTP action. Only HTTP 200 is successful; redirects are rejected.
// +kubebuilder:validation:XValidation:rule="!has(self.basicAuth) || (has(self.scheme) && self.scheme == 'HTTPS')",message="Basic authentication requires HTTPS"
// +kubebuilder:validation:XValidation:rule="!has(self.basicAuth) || !has(self.host) || size(self.host) == 0",message="Basic authentication requires the target Pod address; host must be empty"
// +kubebuilder:validation:XValidation:rule="!has(self.basicAuth) || !has(self.httpHeaders) || self.httpHeaders.all(h, h.name.lowerAscii() != 'authorization')",message="Authorization conflicts with basicAuth"
// +kubebuilder:validation:XValidation:rule="!has(self.tls) || (has(self.scheme) && self.scheme == 'HTTPS')",message="TLS settings require HTTPS"
// +kubebuilder:validation:XValidation:rule="!has(self.host) || (!self.host.contains('@') && !self.host.contains('/') && !self.host.contains('?') && !self.host.contains('#'))",message="host must not contain URL credentials or delimiters"
// +kubebuilder:validation:XValidation:rule="!has(self.path) || (self.path.startsWith('/') && !self.path.startsWith('//'))",message="path must be an absolute endpoint path"
// +kubebuilder:validation:XValidation:rule="!has(self.httpHeaders) || self.httpHeaders.all(h, h.name.lowerAscii() != 'authorization') || (has(self.scheme) && self.scheme == 'HTTPS')",message="Authorization requires HTTPS"
type HTTPAction struct {
HTTPEndpoint `json:",inline"`
// HTTPHeaders contains non-secret custom request headers.
// +optional
// +kubebuilder:validation:MaxItems=32
HTTPHeaders []HTTPActionHeader `json:"httpHeaders,omitempty"`
// Method defaults to GET.
// +optional
// +kubebuilder:validation:Enum=GET;PUT
Method string `json:"method,omitempty"`
// BasicAuth references credentials in the resource namespace. It can only be used with the target Pod address,
// so Host must be empty when BasicAuth is configured.
// +optional
BasicAuth *HTTPBasicAuth `json:"basicAuth,omitempty"`
// TLS configures verified server authentication.
// +optional
TLS *HTTPClientTLS `json:"tls,omitempty"`
}

// HTTPEndpoint identifies a target without Kubernetes GET-only semantics.
type HTTPEndpoint struct {
// Path is the URL path to request. It must be absolute and defaults to "/".
// +optional
// +kubebuilder:validation:MaxLength=2048
Path string `json:"path,omitempty"`
// Port is the numeric or named port on the target Pod.
Port intstr.IntOrString `json:"port"`
// Host is the optional connection host. It defaults to the target Pod's address.
// +optional
// +kubebuilder:validation:MaxLength=253
Host string `json:"host,omitempty"`
// Scheme is the protocol used for the request and defaults to HTTP.
// +optional
// +kubebuilder:validation:Enum=HTTP;HTTPS
Scheme corev1.URIScheme `json:"scheme,omitempty"`
}

// HTTPActionHeader is a bounded non-secret HTTP request header.
type HTTPActionHeader struct {
// Name is the HTTP header name.
// +kubebuilder:validation:MaxLength=256
Name string `json:"name"`
// Value is the HTTP header value.
// +kubebuilder:validation:MaxLength=8192
Value string `json:"value"`
}

// HTTPBasicAuth contains namespace-local credential references.
// +kubebuilder:validation:XValidation:rule="has(self.username.name) && size(self.username.name) > 0 && size(self.username.key) > 0 && (!has(self.username.optional) || !self.username.optional)",message="username requires a non-optional named Secret key"
// +kubebuilder:validation:XValidation:rule="has(self.password.name) && size(self.password.name) > 0 && size(self.password.key) > 0 && (!has(self.password.optional) || !self.password.optional)",message="password requires a non-optional named Secret key"
type HTTPBasicAuth struct {
// Username references the Secret key containing the Basic authentication username.
Username corev1.SecretKeySelector `json:"username"`
// Password references the Secret key containing the Basic authentication password.
Password corev1.SecretKeySelector `json:"password"`
}

// HTTPClientTLS supplies a PEM trust bundle and optional verified DNS name.
// +kubebuilder:validation:XValidation:rule="has(self.caSecret.name) && size(self.caSecret.name) > 0 && size(self.caSecret.key) > 0 && (!has(self.caSecret.optional) || !self.caSecret.optional)",message="TLS requires a non-optional named CA Secret key"
type HTTPClientTLS struct {
// CASecret references the Secret key containing the PEM-encoded CA certificate bundle.
CASecret corev1.SecretKeySelector `json:"caSecret"`
// ServerName is the optional DNS name used to verify the server certificate.
// +optional
ServerName string `json:"serverName,omitempty"`
}

// GetTimeout returns the timeout value in seconds.
func (in *Probe) GetTimeout() time.Duration {
if in == nil || in.TimeoutSeconds == nil || *in.TimeoutSeconds <= 0 {
Expand Down
11 changes: 6 additions & 5 deletions api/v1/coherencejobresource_types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
* Copyright (c) 2020, 2026, Oracle and/or its affiliates.
* Licensed under the Universal Permissive License v 1.0 as shown at
* http://oss.oracle.com/licenses/upl.
*/
Expand Down Expand Up @@ -504,10 +504,11 @@ type CoherenceJobResourceSpec struct {
// +optional
JobAnnotations map[string]string `json:"jobAnnotations,omitempty"`

// ReadyAction is a probe that will be executed when one or more Pods
// reach the ready state. The probe will be executed on every Pod that
// is ready. One the required number of ready Pods is reached the probe
// will also be executed on every Pod that becomes ready after that time.
// ReadyAction is attempted for each Ready Pod after ReadyCount is reached.
// The Operator does not automatically make another attempt while a Pod
// remains Ready. If a Pod stops being Ready and subsequently becomes Ready
// again, another attempt is made. The most recently recorded result is
// available in status.jobProbes.
// +optional
ReadyAction *CoherenceJobProbe `json:"readyAction,omitempty"`

Expand Down
Loading
Loading